Compare commits

...

202 Commits

Author SHA1 Message Date
nou 8b8759facb Delay restoring state of database header 2026-04-13 13:30:54 +02:00
nou 468862ad35 Do not print debug logs by default 2026-04-13 13:29:54 +02:00
nou 974d482d5b Update metainfo 2026-04-12 18:32:15 +02:00
nou d45bf37e50 Update translation 2026-04-12 18:23:48 +02:00
nou 56a8a0e789 Special handling of COUNT aggregate function 2026-04-12 18:23:39 +02:00
nou 27afb2ea5f Add table view to database tree 2026-04-12 13:41:29 +02:00
nou 28016ada8d Improve logging 2026-04-12 10:26:03 +02:00
nou 885a5b4c6d Add support copy to clipboard for table 2026-04-12 10:20:43 +02:00
nou 63149745ed Handle return value of QFile::open 2026-04-12 10:19:40 +02:00
nou ef8b3d7668 Add scriptarg to cli options 2026-04-10 21:55:36 +02:00
nou 8d2a0a28cc Update tranlations 2026-04-10 21:39:37 +02:00
nou 6ba9be41ec Add database tree view 2026-04-08 19:55:26 +02:00
nou 65fca14ac2 Add running script as CLI option 2026-04-08 19:38:52 +02:00
nou 3818fd4625 Update copyright year 2026-03-30 22:31:51 +02:00
nou 3f88e5fe83 Reopen console on windows when started from cmd.exe 2026-03-26 14:46:23 +01:00
nou 6a537642ab Add copy files to database view 2026-03-22 21:10:04 +01:00
nou b7f1a0abc9 Add id_file_key index to database 2026-03-22 09:55:19 +01:00
nou 33c976d3c9 Remember a selected filter keyword in database view 2026-03-22 09:54:57 +01:00
nou a17001cdf9 Trim whitespace string from XISF 2026-03-21 22:21:35 +01:00
nou 305c1d1f55 Deffered SQL query when database is visible 2026-03-21 20:33:47 +01:00
nou 95808b094d Fix buidling query 2026-03-21 20:31:44 +01:00
nou 2b56af27fe Add explicit link to Svg module to solve some issues with SVG icon 2026-03-15 17:47:51 +01:00
nou 8edf746827 Use bindvalue in DatabaseTableView 2026-03-15 17:47:24 +01:00
nou 729a330e6c Add backspace as move to trash shortcut for MacOS 2026-03-15 17:45:01 +01:00
nou 1ac5a4e42a Update metainfo 2026-02-16 22:52:28 +01:00
nou 83d212aa91 Enable sorting of FITS header 2026-02-16 22:29:25 +01:00
nou bd24fba407 Update README 2026-02-11 21:26:33 +01:00
nou 3448f62f31 Try to fix crash in ImageRingList 2026-01-19 20:57:13 +01:00
nou 567e66acb5 Update libXISF 2025-11-02 23:17:10 +01:00
nou 9e79133464 Fix compile error 2025-11-01 12:11:53 +01:00
nou e08107aa13 Improve Save as 2025-11-01 12:06:24 +01:00
nou 6eda2c4e48 Remove some code 2025-10-20 23:48:47 +02:00
nou b16ae3a9ee Add language setting 2025-10-20 21:29:59 +02:00
nou 56bba27ae3 Give save filter only formats that are supported 2025-10-20 00:23:11 +02:00
nou 1070dc32c1 Update metainfo 2025-09-15 15:55:00 +02:00
nou f61cf12f0a Update help files 2025-09-15 15:49:30 +02:00
nou 530b0c62c3 Open parent directory if it doesn't exist 2025-09-15 15:49:16 +02:00
nou 7e95440dd6 Update translations 2025-09-15 10:58:29 +02:00
nou 03492972cb Include all types in completion 2025-09-14 20:44:36 +02:00
nou 9cca183677 Working copy/move operation 2025-09-14 20:44:07 +02:00
nou afd059b36b Add some margins when retriving objects 2025-09-14 13:43:20 +02:00
nou 1b9f218acb Console line with simple auto completion 2025-08-17 17:39:01 +02:00
nou 32f91d7b2f Add PCL:AstrometricSolution to XISF solved files 2025-08-03 20:58:01 +02:00
nou 69fbad34b6 Add image index to FITSRecordModify 2025-08-03 20:57:33 +02:00
nou e026042604 Use ifFITS isXISF functions 2025-08-03 20:37:40 +02:00
nou bb7e5182af Fix metainfo linter 2025-07-27 17:45:23 +02:00
nou f0152e2496 Fix compilation issue 2025-07-27 17:33:14 +02:00
nou eccf928032 Make path in file manager editable 2025-07-27 16:29:44 +02:00
nou 897306d1c3 Add fitskeyword.ui 2025-07-27 15:40:06 +02:00
nou cbc779090f Prioritize M number then IC for object name 2025-07-27 15:36:19 +02:00
nou d826744f26 Add tabs to file manager 2025-07-27 15:35:25 +02:00
nou 3bdfb12d4f Disable use of QFileSystemModel on ARM platform 2025-07-27 10:06:38 +02:00
nou c416ae9941 Fix suffix handling, do not index PCL: properties 2025-07-26 18:19:46 +02:00
nou abbba2890f Add context menu to hide columns in file manager 2025-07-26 17:50:54 +02:00
nou 9c6847d334 Do not pass XISF properies into WCS 2025-07-26 15:49:21 +02:00
nou af1c26a9fe Use PCL:AstrometricSolution properties to construct WCS 2025-07-26 15:48:58 +02:00
nou e0441a6494 Change type to uint64_t 2025-07-24 20:22:06 +02:00
nou a88f05a9fe Add filemanager 2025-07-21 20:16:34 +02:00
nou b58559a18a Update modify FITS header script 2025-07-13 10:43:15 +02:00
nou 2ac14a6c04 Fix thumbnailer compilation 2025-07-13 10:42:59 +02:00
nou b84256625c Add stellarsolver6 as name 2025-06-12 16:48:09 +02:00
nou 202a2b11b7 Add marked files in batch processing 2025-06-09 19:09:50 +02:00
nou 32f192ed7e Add draw grid button 2025-05-31 00:19:15 +02:00
nou a0422683bd Move source files to src directory 2025-05-30 16:49:33 +02:00
nou ce67b35bfa Mention OpenNGC 2025-05-29 23:44:55 +02:00
nou f016500f12 Include ngc db 2025-05-29 17:39:13 +02:00
nou 6069ebbbac Refractor drawing grid 2025-05-27 16:26:03 +02:00
nou e587d84e05 Remove that empty action 2025-05-26 17:05:19 +02:00
nou c01f2e328a Add sky grid painting 2025-05-26 15:50:37 +02:00
nou 8b498bbe73 Prefer writing keyword as integer 2025-05-24 23:13:14 +02:00
nou c6bc792ff7 Update metainfo 2025-04-29 16:03:30 +02:00
nou 1a307d82f9 Update translations 2025-04-29 15:59:59 +02:00
nou 8c5e2b2ebf Update help 2025-04-29 14:07:16 +02:00
nou 03ad135ef0 Fix bestFit line color 2025-04-29 13:31:42 +02:00
nou 2a78a9a41d Add XISF mime type 2025-04-29 13:15:45 +02:00
nou 1a214a169e Blind and update question for platesolve script 2025-04-28 17:25:07 +02:00
nou f8704c51d8 FITS hightlight settings 2025-04-28 17:06:24 +02:00
nou 3feee0256c New core.question script method 2025-04-28 17:04:45 +02:00
nou 53472d807c Adding new scripts 2025-04-27 22:54:29 +02:00
nou 9f06269aa4 Improve chart graph 2025-04-27 17:43:32 +02:00
nou 78f242d808 Extending script plot() function 2025-04-23 13:37:47 +02:00
nou e6bab45a89 Fix index of subimage for XISF 2025-04-15 20:58:03 +02:00
nou 58286d52c5 TextFile scripting 2025-04-15 11:09:23 +02:00
nou bac1963fa4 Add stretch toolbar actions to view menu 2025-04-11 17:05:31 +02:00
nou 2415717ce0 Add STFSlider ability to be vertical 2025-04-10 23:09:59 +02:00
nou e7acbca01e Color highlight FITS header 2025-04-10 19:58:29 +02:00
nou 7c4118b0b6 Fix bug in script solver 2025-04-10 00:34:14 +02:00
nou 8178efdafd Reload image when header is updated 2025-04-09 14:58:23 +02:00
nou 90026f931f Add open file and open file location to DB view 2025-04-02 20:27:59 +02:00
nou eee4613b25 Add plot() script method 2025-04-02 20:27:19 +02:00
nou 24a9e96bbf Streamline standalone thumbnailer 2025-04-02 15:24:41 +02:00
nou 5af5f4f068 Navigation menu 2025-04-02 12:24:17 +02:00
nou 85f9822b96 Fix prevSubImage 2025-03-25 18:26:08 +01:00
nou 7fc6c64fd7 Make help windows non modal 2025-03-24 22:09:18 +01:00
nou 4488c2e6af Always make 4 channels 2025-03-24 22:08:54 +01:00
nou 0047607c1d Add loading sub images 2025-03-23 13:33:34 +01:00
nou 45c368bbbb Remove usage of SLOT() and SIGNAL() 2025-03-19 13:50:39 +01:00
nou c96cb86a29 Show relative path in title bar for when browsing dir recursive 2025-03-19 13:14:04 +01:00
nou fe3e5f66be Release 20250318 2025-03-18 17:29:19 +01:00
nou 6fd17fbdf5 Use only single database 2025-03-18 14:46:08 +01:00
nou f30dd2a520 Add generating thumbnails from cmd line 2025-03-17 11:08:18 +01:00
nou 21675d9479 Add install thumbnailer button 2025-03-17 11:07:42 +01:00
nou f669baa8a6 Drop lib prefix for dll 2025-03-10 11:26:43 +01:00
nou c317012c99 Configurable threshold 2025-03-10 03:05:44 -07:00
nou d0dbef20c7 Fix handling of inf and nan in TFloat16 2025-03-10 11:03:17 +01:00
nou bd45900821 Finish standalone thumbnailer under 2025-03-10 11:01:45 +01:00
nou 96a89bff92 fixup! Fix images that have values outside of 0-1 range 2025-03-09 17:51:26 +01:00
nou c05fc36ee3 Add FITS to thumbnailer 2025-03-06 18:36:48 +01:00
nou 05b0aa9a2f Add custom implementation of half float 2025-03-06 15:35:12 +01:00
nou 7b70b6cce5 Use texture2DArray for colormap to work with OpenGL ES 2025-03-05 21:14:43 +01:00
nou 5150ec5639 Fix images that have values outside of 0-1 range 2025-03-04 16:09:33 +01:00
nou 79529552d9 Thumbnailer for windows 2025-03-04 06:53:47 -08:00
nou c872c72bb5 Release 20250302 2025-03-02 17:31:24 +01:00
nou 58abf762c0 Fix Do not prepend \\ to network share paths 2025-03-02 17:09:33 +01:00
nou e47c99fd21 Update translations 2025-03-02 14:41:31 +01:00
nou 24ddf1dc61 Add ability to have user defined colormaps 2025-03-02 14:32:09 +01:00
nou 8f333191c3 Update help 2025-03-02 14:31:29 +01:00
nou e4cb99657e Move aspect to resize in parameters 2025-03-02 13:45:58 +01:00
nou d644e8095d Small fix for help dialog 2025-03-02 13:45:39 +01:00
nou 1796e128ad For setSolverProfile use index 1-8 2025-03-02 13:45:31 +01:00
nou 37fdac39dc Add best fit on image load 2025-03-02 13:44:59 +01:00
nou 13e1abf07e Add autotrech to conversion 2025-03-01 14:24:28 +01:00
nou 617abf7afe Better handling of FITS loading error 2025-02-28 10:42:39 +01:00
nou d59ee7fddc Add additional colormaps 2025-02-28 10:41:12 +01:00
nou d545c6ca0f Do not prepend \\ to network share paths 2025-02-26 20:43:46 +01:00
nou 5249b277ec Add integer resample 2025-02-25 17:41:29 +01:00
nou e4b9fefa5a Move MTFParam 2025-02-25 17:35:55 +01:00
nou d069ce3302 Add shortcut to 100% zoom 2025-02-23 09:45:29 +01:00
nou 58c182adc0 Add thumbnailer 2025-02-16 23:36:25 +01:00
nou c36068aaf4 Fix mxe build 2025-02-16 16:19:39 +01:00
nou fcb3aec81f Remove startfit 2025-02-16 15:22:38 +01:00
nou 7510dac82b Reorginize code 2025-02-16 15:19:20 +01:00
nou 55439be04c Add Portuguese translation thanks to John Peter Sá 2025-02-07 15:10:19 +01:00
nou 0ff2001797 Handle MAX_PATH every where 2025-01-26 16:00:00 +01:00
nou fc36024eee Remove deprecated QSqlQuery usage 2025-01-19 15:49:31 +01:00
nou 3cda53f26c Do not attempt open not existing file 2025-01-19 15:11:32 +01:00
nou 58d18cc28a Remove unused function 2025-01-19 15:11:14 +01:00
nou 2b96da60de Do not use global thread pool 2025-01-12 10:59:12 +01:00
nou 236f66ed2f Add solver profile to script engine 2024-12-27 23:20:51 +01:00
nou a86c100e69 Fix issue with Qt 6.8 2024-12-22 12:19:34 +01:00
nou 45ee9b7258 Fix half pixel offset and add filtering in sw rendering
Signed-off-by: Dušan Poizl <nou.spiro@gmail.com>
2024-12-05 16:02:46 +01:00
nou be1e65251d Support really big images 50000px 2024-11-30 22:03:58 +01:00
nou 9b7837e9fb SW rendering when image is too big for texture 2024-11-27 20:21:57 +01:00
nou 4afa940886 Update metainfo 2024-11-16 22:58:34 +01:00
nou d1344d2dc8 Add support for uint32 and double in boxResample 2024-11-15 23:29:22 +01:00
nou 24eea573e6 Handle all data types when converting to QImage 2024-10-29 19:43:16 +01:00
nou 8f7f527732 Add RawImage::convertToType() 2024-10-29 19:42:36 +01:00
nou 3635ac00cb Fix alpha channel in fromPlanarSSE 2024-10-29 19:39:37 +01:00
nou eba9110933 Remove dead code 2024-10-29 15:32:06 +01:00
nou 464207beb1 Apply index folder directory change immediatly 2024-10-24 20:15:06 +02:00
nou 4aeff61c44 Add TIFF as valid format for script convert 2024-10-13 19:58:13 +02:00
nou 790c836bbd MXE build 2024-10-13 19:58:12 +02:00
nou 62616898ed Forgot to add fts and fz to one place 2024-10-13 19:57:21 +02:00
nou e216af6a6d Better handling of missing and overwrite files 2024-10-13 19:57:21 +02:00
nou e0d6f417a0 fixup! Fix compiling without stellarsolver 2024-10-04 20:51:53 +02:00
nou 3c5fef988e Fix compiling without stellarsolver 2024-10-02 15:25:32 +02:00
nou 6c42315f87 Update metainfo 2024-10-02 11:58:39 +02:00
nou 06b3dbc1bb Update help and translation 2024-10-02 11:52:35 +02:00
nou 258553a6bb Change url for downloading 2024-10-02 10:08:50 +02:00
nou 9c40ce2daa Add open marked files 2024-10-02 00:10:07 +02:00
nou 9f4c4c8bdc Refining platesolving 2024-10-01 17:37:34 +02:00
nou da1aa4c6fc Updating FITS header 2024-09-30 21:19:23 +02:00
nou a43f12565d Add FTS and FZ as FITS file suffix 2024-09-30 19:56:53 +02:00
nou 32973c54ce Adding platesolving 2024-09-30 18:39:35 +02:00
nou dccb2e88da Working solver 2024-09-20 14:34:21 +02:00
nou c8898387fe Http download of index files 2024-09-19 15:28:57 +02:00
nou dfe31b6350 In live mode reload immediatly 2024-09-19 13:40:41 +02:00
nou 553e72a5ce Add check for new version 2024-09-19 13:40:41 +02:00
nou 12901c9a47 Fix that comment in FITS record contained value 2024-09-17 23:06:16 +02:00
nou da79197376 Initial stellarsolver implementation 2024-09-17 23:05:27 +02:00
nou 30960033c5 Refractor ImageScrollArea 2024-09-17 12:31:55 +02:00
nou efd3ff35f3 Fix unpack aligment 2024-09-14 23:12:50 +02:00
nou 52bcb10da1 Proper LCMS2 link don't use OpenGL ES on MacOS 2024-08-27 15:27:27 +02:00
nou 9adfbde512 Remember auto stretch on load 2024-08-27 13:43:17 +02:00
nou e38de510a0 Open also file:// 2024-08-27 13:17:53 +02:00
nou 5e18c591f7 Add support for FLOAT16 image downscale 2024-08-27 11:39:47 +02:00
nou d6e257e201 Use flatpak trash portal instad of g_file_trash 2024-08-26 11:43:57 +02:00
nou 2c7a7d473f Fix output path browse dialog path 2024-08-26 10:19:48 +02:00
nou 87d7bd2d9f Add delay to directory reload 2024-08-25 22:20:41 +02:00
nou 79dd7d91eb Fix fromPlanaerSSE 2024-08-25 17:55:59 +02:00
nou 21b4e0934c Set GL_UNPACK_ALIGNMENT 2024-08-25 17:55:47 +02:00
nou 0239aba165 Debayer fix on OpenGL ES 2024-08-25 14:39:07 +02:00
nou 1b08242433 Use OpenGL ES on ARM by default 2024-08-25 12:49:14 +02:00
nou a56e8a2c27 Fix bug with thumbnails on OpenGL ES 2024-08-25 12:41:12 +02:00
nou bf360c1ae1 Fix thumbnails draw 2024-08-24 18:17:41 +02:00
nou 1ec3a6cffd Revert change in app id 2024-08-24 17:49:01 +02:00
nou 100f47746c Fix path 2024-08-24 17:14:10 +02:00
nou 02bac0c850 Fix compiler error with float16 2024-08-24 17:14:02 +02:00
nou bc29dc7d34 Use lcms2 for color profiles 2024-08-24 16:37:06 +02:00
nou ff5053b626 Added missing case FLOAT16 2024-08-22 19:10:26 +02:00
nou 511802bdbd Usable OpenGL ES 2024-08-22 17:54:26 +02:00
nou dd16a02045 Preparation for OpenGL ES 2024-08-20 17:53:30 +02:00
nou 7ed38cf6d7 Additional missing dependencies 2024-08-20 17:51:52 +02:00
nou 8c6b451564 Suggestion by Der_Pit 2024-08-17 19:56:01 +02:00
nou d288810d5d Fix saving image 2024-08-16 15:16:37 +02:00
nou fb66e82428 Workaround for huge PCL keywords 2024-08-16 15:04:25 +02:00
nou 71486efeef Remove unused method 2024-06-17 09:47:56 +02:00
nou 8213f6213f Fix convert scripts 2024-06-17 09:44:48 +02:00
nou f8c9fec77e Add scripts 2024-06-16 16:31:53 +02:00
nou af4be850cb Embeded scripts 2024-06-16 00:14:51 +02:00
nou ca1a13ed9d Solve deprectation warnings 2024-06-15 17:45:30 +02:00
nou 1873da6c49 Fix issue in thumbnails 2024-06-11 17:09:35 +02:00
120 changed files with 18193 additions and 4106 deletions
+73 -41
View File
@@ -17,50 +17,55 @@ if(SANITIZE_ADDRESS_LEAK)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak")
endif(SANITIZE_ADDRESS_LEAK)
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml REQUIRED)
find_library(GSL_LIB gsl REQUIRED)
find_library(GSLCBLAS_LIB gslcblas REQUIRED)
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml Charts Svg REQUIRED)
find_library(EXIF_LIB exif REQUIRED)
find_library(FITS_LIB cfitsio REQUIRED)
find_library(RAW_LIB NAMES raw_r REQUIRED)
find_library(WCS_LIB wcs wcslib PATHS REQUIRED)
find_library(WCS_LIB wcs wcslib REQUIRED)
find_library(LCMS2_LIB lcms2 REQUIRED)
find_library(STELLARSOLVER_LIB NAMES stellarsolver stellarsolver6)
add_subdirectory(libXISF)
set(TENMON_SRC
about.cpp about.h
batchprocessing.cpp batchprocessing.h batchprocessing.ui
database.cpp database.h
databaseview.cpp databaseview.h
delete.cpp
filesystemwidget.cpp filesystemwidget.h
histogram.cpp histogram.h
imageinfo.cpp imageinfo.h
imageringlist.cpp imageringlist.h
imagescrollarea.cpp
imagescrollareagl.cpp imagescrollareagl.h
loadrunable.cpp loadrunable.h
main.cpp
mainwindow.cpp mainwindow.h
markedfiles.cpp markedfiles.h
rawimage.cpp rawimage.h
rawimage_sse.cpp
scriptengine.cpp scriptengine.h
settingsdialog.cpp settingsdialog.h
starfit.cpp starfit.h
statusbar.cpp statusbar.h
stfslider.cpp stfslider.h
stretchtoolbar.cpp stretchtoolbar.h
src/about.cpp src/about.h
src/batchprocessing.cpp src/batchprocessing.h src/batchprocessing.ui
src/chartgraph.h src/chartgraph.cpp
src/database.cpp src/database.h
src/databasetree.cpp src/databasetree.h
src/databasetreekeys.ui
src/databaseview.cpp src/databaseview.h
src/delete.cpp
src/filemanager.h src/filemanager.cpp src/filemanager.ui
src/filesystemwidget.cpp src/filesystemwidget.h
src/fitskeyword.ui
src/histogram.cpp src/histogram.h
src/httpdownloader.h src/httpdownloader.cpp
src/imageinfo.cpp src/imageinfo.h
src/imageinfodata.cpp src/imageinfodata.h
src/imageringlist.cpp src/imageringlist.h
src/imagescrollarea.cpp src/imagescrollarea.h
src/imagewidget.h src/imagewidget.cpp
src/loadimage.h src/loadimage.cpp
src/loadrunable.cpp src/loadrunable.h
src/main.cpp
src/mainwindow.cpp src/mainwindow.h
src/markedfiles.cpp src/markedfiles.h
src/mtfparam.h
src/rawimage.cpp src/rawimage.h
src/rawimage_sse.cpp
src/scriptengine.cpp src/scriptengine.h
src/settingsdialog.cpp src/settingsdialog.h
src/statusbar.cpp src/statusbar.h
src/stfslider.cpp src/stfslider.h
src/stretchtoolbar.cpp src/stretchtoolbar.h
src/tfloat16.h
thumbnailer/genthumbnail.cpp thumbnailer/genthumbnail.h
)
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
if(COLOR_MANAGMENT)
add_compile_definitions("COLOR_MANAGMENT")
endif(COLOR_MANAGMENT)
qt_add_resources(TENMON_SRC resources/resources.qrc)
qt_add_resources(TENMON_SRC shaders/shaders.qrc)
qt_add_resources(TENMON_SRC scripts/scripts.qrc)
if(WIN32)
list(APPEND TENMON_SRC resources/icon.rc)
set(tenmon_ICON "")
@@ -71,24 +76,43 @@ elseif(APPLE)
else()
set(tenmon_ICON "")
find_package(Qt6 COMPONENTS DBus REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_search_module(GIO REQUIRED gio-2.0)
endif()
qt_add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED)
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR})
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR} "src")
if(UNIX AND NOT APPLE)
target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS})
endif()
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
if(COLOR_MANAGMENT)
target_compile_definitions(tenmon PRIVATE "COLOR_MANAGMENT")
endif(COLOR_MANAGMENT)
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} XISF)
find_path(STELLARSOLVER_INCLUDE stellarsolver.h PATH_SUFFIXES libstellarsolver)
if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
target_include_directories(tenmon PRIVATE ${STELLARSOLVER_INCLUDE})
if(MXE)
find_library(GSL_LIB gsl REQUIRED)
find_library(GSLCBLAS_LIB gslcblas REQUIRED)
target_compile_definitions(tenmon PRIVATE "stellarsolver_STATIC")
target_link_libraries(tenmon PRIVATE ${STELLARSOLVER_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} boost_regex-mt-x64)
else(MXE)
target_link_libraries(tenmon PRIVATE ${STELLARSOLVER_LIB})
endif(MXE)
target_compile_definitions(tenmon PRIVATE "PLATESOLVER")
target_sources(tenmon PRIVATE
src/solver.cpp src/solver.h
src/platesolving.cpp src/platesolving.h src/platesolving.ui
src/platesolvingsettings.cpp src/platesolvingsettings.h src/platesolvingsettings.ui
)
message(STATUS "Found stellarsolver ${STELLARSOLVER_INCLUDE} ${STELLARSOLVER_LIB}")
endif(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml Qt6::Charts Qt6::Svg ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF)
if(APPLE)
target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation")
elseif(UNIX)
target_link_libraries(tenmon PRIVATE Qt6::DBus ${GIO_LDFLAGS})
target_link_libraries(tenmon PRIVATE Qt6::DBus)
endif(APPLE)
if(LIBRAW_STATIC)
@@ -96,6 +120,11 @@ if(LIBRAW_STATIC)
target_link_libraries(tenmon PRIVATE jasper)
endif()
option(FLATPAK "Flatpak build" OFF)
if(FLATPAK)
target_compile_definitions(tenmon PRIVATE FLATPAK)
endif(FLATPAK)
install(TARGETS tenmon BUNDLE DESTINATION .)
if(UNIX AND NOT APPLE)
include(GNUInstallDirs)
@@ -106,6 +135,7 @@ if(UNIX AND NOT APPLE)
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
install(FILES resources/space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
install(FILES resources/space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
install(FILES space.nouspiro.tenmon.xisf.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/mime/packages")
endif()
install(FILES space.nouspiro.tenmon.metainfo.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
endif(UNIX AND NOT APPLE)
@@ -119,3 +149,5 @@ else()
execute_process(COMMAND ${CMAKE_COMMAND} -Dlocal_dir=${CMAKE_CURRENT_SOURCE_DIR} -Doutput_dir=${CMAKE_CURRENT_BINARY_DIR}
-P "${CMAKE_CURRENT_SOURCE_DIR}/gitversion.cmake")
endif()
add_subdirectory(thumbnailer)
+18 -5
View File
@@ -2,23 +2,36 @@ FITS/XISF image viewer with multithreaded image loading
To get all dependencies install these packages
sudo apt install qt6-base-dev qt6-declarative-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev libgsl-dev wcslib-dev cmake
sudo apt install qt6-base-dev qt6-declarative-dev qt6-charts-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev wcslib-dev cmake libzstd-dev libqt6sql6-sqlite
on OpenSUSE
sudo zypper install gsl-devel exif-devel libraw-devel wcslib-devel libqt6-qtbase-devel
sudo zypper install libexif-devel libraw-devel wcslib-devel qt6-base-devel qt6-qml-devel libzstd-devel
MacOS X
To compile on MacOS install XCode first. Then install homebrew in x86_64 mode
with "arch -i x86_64". Building on native ARM is not supported.
To compile on MacOS install XCode first. Then install homebrew.
homebrew install qt6 libraw cfitsio libexif libgsl wcslib
You may need to set CMAKE_PREFIX_PATH for Qt6 so CMake can find them.
Then to build run standard cmake
First run this command to get libXISF updated
git submodule update --init --recursive
Then to build run standard cmake sequence
cmake -B build -S .
cmake --build build
./build/tenmon
To install it to system run this command as root
cmake --install build
For working plate solving you must have compiled and installed StellarSolver https://github.com/rlancaste/stellarsolver
It is important that you compile StellarSolver with Qt6. By default it use Qt5 but when linked with Qt6 program it will
crash.
Using OpenNGC database https://github.com/mattiaverga/OpenNGC under CC-BY-SA-4.0 https://creativecommons.org/licenses/by-sa/4.0/
+162 -33
View File
@@ -1,5 +1,7 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<title>Help</title>
<style type="text/css">
h1, h2, h3, h4 { padding:0px; margin:10px; }
p { padding:0px; margin:5px; }
@@ -9,14 +11,13 @@ img { margin: 5px; }
<body>
<h2>Tenmon help</h2>
<p>Tenmon is intended primarily for viewing astro photos and images. It supports the following formats:
<p>Tenmon is intended primarily for viewing astro photos and images. It supports the following formats:</p>
<ul>
<li>FITS 8, 16, 32 bit integer and 32, 64 bit float</li>
<li>XISF 8, 16, 32 bit integer and 32, 64 bit float</li>
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
<li>CR2, CR3, NEF, DNG raw images</li>
</ul>
</p>
<h3>Main windows</h3>
<p>The main window shows the currently loaded image. On the left is the <i>Image info</i> panel which displays details about the loaded image.
@@ -35,26 +36,32 @@ In the case of saving JPEG or PNG, the stretch function is applied to the saved
To open an image, you can also drag and drop it to main window.</p>
<h3>View</h3>
<p>The <i>View</i> menu has options to control the size and scale of displayed images:
<p>The <i>View</i> menu has options to control the size and scale of displayed images:</p>
<ul>
<li><i>Zoom In</i> and <i>Zoom Out</i> magnify and shrink the image. The mouse wheel can be also used to zoom freely.</li>
<li><i>Best fit</i> auto-zooms the image to fit the current size of the window.</li>
<li><i>100%</i> will zoom to 1:1 scale.</li>
<li><i>Bayer mask</i> set which bayer mask should be used when doing demosaicing.</li>
<li><i>Colormap</i> select color pallette when showing image with false colours.</li>
<li><i>Fullscreen</i> enlarges the main window to the whole screen.</li>
<li><i>Thumbnails</i> will display small thumbnails for all images in the current directory.</li>
<li><i>Slideshow</i> start showing all images periodically with interval that can be set in settings.</li>
</ul>
</p>
<p>Colormap can also be user defined. Place image file named colormap.png into application data directory.
On Windows"C:/Users/&lt;USER&gt;/AppData/Roaming/nou/Tenmon" Linux: "~/.local/share/nou/Tenmon/" MacOS: "~/Library/Application Support/nou/Tenmon/"
This image should be 256 pixel wide. Each row of image will be used as separate color map and added to Colormap menu.</p>
<h3>Stretch toolbar</h3>
<p>This panel changes how images are displayed.
<br><img src=":/about/stretch-panel.png"></p>
<p>Starting on the left, there is slider scale with three adjustable points to manually control the stretch.
<br><img src=":/about/stretch-panel.png" alt="Stretch panel"></p>
<p>Starting on the left, there is slider scale with three adjustable points to manually control the stretch.</p>
<ul>
<li>black point - all pixels with lower value (darker) than this setting will be clipped black</li>
<li>mid point - defines the value to be stretched to 50% intensity</li>
<li>white point - all pixels with higher value (brighter) than this will be clipped white</li>
</ul>
Following the slider are 7 buttons for automatic stretching:
<p>Following the slider are 8 buttons to control image display:</p>
<ul>
<li><i>Linked channels</i> toggle stretching each RGB channel individually.</li>
<li><i>Auto Stretch</i> automatically apply black and mid points to render the image with optimal brightness.</li>
@@ -62,7 +69,8 @@ Following the slider are 7 buttons for automatic stretching:
<li><i>Invert</i> invert colors to display the image as negative.</li>
<li><i>False colors</i> show black and white in false colour palette.</li>
<li><i>Debayer CFA</i> Demosaicing black and white image from CFA sensor to color one.</li>
<li><i>Apply Auto stretch on load</i> toggle automatically applying Autostretch for each image when loaded.</p>
<li><i>Apply Auto stretch on load</i> toggle automatically applying Autostretch for each image when loaded.</li>
<li><i>Draw equatorial grid</i> toggle drawing equatorial coordinate grid and catalogue objects. Needs that file have WCS data.</li>
</ul>
<h3>Marking images</h3>
@@ -77,13 +85,12 @@ mouse button and drag across thumbnails to mark them. Holding <i>Ctrl</i> will u
<h3>File system and tree</h3>
<p>File system panel contain list of images in current opened directory. You can select file from this list and it will be displayed. It is also possible to
use arrow keys to go back (left and up) and forth (right and down) between images.</p>
<p>File tree show file system structure. You can right click to show context menu to perform various actions from <i>File</i> menu. There are also few others
<p>File tree show file system structure. You can right click to show context menu to perform various actions from <i>File</i> menu. There are also few others</p>
<ul>
<li><i>Set as root directory</i> show only this directory and subdirectories</li>
<li><i>Reset root directory</i> show whole file system</li>
<li><i>Go up</i> show directory that is one level above current root directory</li>
</ul>
</p>
<h3>Database of FITS/XISF files</h3>
<p>Tenmon can scan a directory of FITS/XISF files and index metadata from FITS headers into it's internal database. This allows searching and sorting images based on that metadata.</p>
@@ -105,23 +112,77 @@ Setting both "RA pos" and "DEC pos" can return images that doesn't contain enter
"RA range" and "DEC range" filter out images which center coordinate is within entered range.
Pressing Enter or clicking on <i>Filter</i> button will filter out database record according to search parameter.
<p>Wildcards:
<p>Wildcards:</p>
<ul>
<li><b>%</b> (percent) is a wildcard representing zero or more of any characters.</li>
<li><b>_</b> (underscore) is a wildcard for exactly one of any character.</i>
<li><b>_</b> (underscore) is a wildcard for exactly one of any character.</li>
<li>Without wildcard characters, the exact string must match.</li>
</ul>
</p>
<br><img src=":/about/filter.png"><br>
<p><img src=":/about/filter.png" alt="Filter"><br>
This example filters for files where: "Bias" is in the file name, the OBJECT property is "M_42" (where the underscore can be any single character), and the DATE property begins with "2022".
</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>This module can plate solve images and update FITS header with solution for FITS and XISF images.
<b>Profile</b> this set various parameters that affect star extraction and solving.
<b>Starting point</b> program will try to automatically determine optimal starting point which helps to speed up solving.
You can leave one or both unchecked then it will attempt to do blind solving. If the position or scale is wrong it can actually
fail to solve.
<b>Solution</b> this section contain resulting solution like RA,DEC coordinates center of image, image field of view, orientation as degrees E of N,
image scale in arcseconds per pixel, number of stars extracted and HFR fitting and eccentricity. Then there is log window for debug information from
solver.
</p>
<p>Then finally there are various action button. Settings button show dialog where you can set path to existing index files or auto download some.
Extract button will just extract stars from image and it will show their count, HFR and eccentricity. This action doesn't need index files.
Solve button will try to find coordinates of images. Abort button will stop extraction or solving. Update FITS header will update FITS fits keywords
with found solution.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Batch processing</h3>
This module allow to write scripts in JavaScript that process image files. Batch Processing window consist from three main parts. On top is list of input files and directories.
<p>This module allow to write scripts in JavaScript that process image files. Batch Processing window consist from three main parts. On top is list of input files and directories.
You can add directories or individual files to this list. Directories are scanned recursively to find all files even non image files. This list of files is then passed to script in array named <b>files</b>.
In script you can then iterate through files like this.
In script you can then iterate through files like this.</p>
<pre>for(file of files)
{
if(file.suffix() == "fits")
@@ -135,12 +196,13 @@ In script you can then iterate through files like this.
this output directory is ignored.</p>
<p>Bellow that is list of scripts. These scripts are located in application data directory. Select script which you want to run by clicking on it. Clicking on <i>Open scripts</i> will open directory with these scripts where you create new and edit them.
Location of this directory is on Windows: "C:/Users/<USER>/AppData/Roaming/nou/Tenmon" Linux: "~/.local/share/nou/Tenmon/scripts" MacOS: "~/Library/Application Support/nou/Tenmon/scripts"</p>
Location of this directory is on Windows: "C:/Users/&lt;USER&gt;/AppData/Roaming/nou/Tenmon/scripts" Linux: "~/.local/share/nou/Tenmon/scripts" MacOS: "~/Library/Application Support/nou/Tenmon/scripts"</p>
<p>Next is Log windows that contain any messages that come from scripts. Mainly calls to <code>core.log()</code> At bottom there buttons that can start or stop execution of selected scripts.</p>
<p>Next is Log windows that contain any messages that come from scripts. Mainly calls to <code>core.log()</code> At bottom there is console that enable run simple script commands and
buttons that can start or stop execution of selected scripts.</p>
<h4>core</h4>
There is global object called <b>core</b> that have these methods.
<p>There is global object called <b>core</b> that have these methods.</p>
<ul>
<li><b>log(message)</b> print message to log window.</li>
<li><b>mark(file)</b> mark file same way as in GUI. Takes object of type <i>File</i> as argument.</li>
@@ -155,10 +217,59 @@ There is global object called <b>core</b> that have these methods.
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> show dialog box with input box to retrieve decimal value. String value passed in first argument is used as description label.
Second parameter is default value in input box. All three parameters are optional. When cancel is pressed it return Undefined.</li>
<li><b>getItem(items)</b> show selection dialog which allow to select one item from array of items. It return selected item as string. When cancel is pressed it return Undefined.</li>
<li><b>setStartingSolution(solution)</b> with this you can set starting point and image scale. It accepth object with attributes "ra", "dec", "pixscale".
Same object as returned by <i>File.solve()</i> method. You can also call it without paramer in which case it will clear any previously set values.</li>
<li><b>getSolverProfile()</b> return solver profile as Object.
<pre>var profile = core.getSolverProfile();
core.log(JSON.stringify(profile));</pre></li>
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
It return button that was clicked as a string.
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
</li>
<li><b>plot(graph)</b> this method show graph defined by JS object.
<pre>
var chart = {
"title": "Chart title", // Title that will show on top of chart
"legend":
{
"visible": true,// default is true
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
},
"series":[ // array of data series
{
"title": "HFR",
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
"y":[2.5,3.1,2.6,2.2] // array of values
},
{
"title": "Ecc",
"y":[0.37, 0.4, 0.35, 0.25],
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
},
{
"title": "Stars",
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
"shape": "star", // shape of markers. valid only for points
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
"y":[523,412,487,510],
"y2": true, // if set to true this serie will use secondary Y axis
"bestFit": true, // show best fit line
"color": "#0000ff"
}
]
};
core.plot(chart);
</pre>
</li>
</ul>
<h4>File</h4>
In <b>files</b> array there are instances of type <b>File</b> objects that have these methods.
<p>In <b>files</b> array there are instances of type <b>File</b> objects that have these methods.</p>
<ul>
<li><b>fileName()</b> returns the name of the file, excluding the path.</li>
<li><b>absoluteFilePath()</b> returns an absolute path including the file name.</li>
@@ -176,23 +287,33 @@ In <b>files</b> array there are instances of type <b>File</b> objects that have
<li><b>fitsRecords()</b> return array of objects with properties <b>key, value</b> and <b>comment</b> </li>
<li><b>modifyFITSRecords(FITSRecordModify)</b> modify FITS header by adding, removing or updating FITS record. Return true on success. Refer to <i>FITSRecordModify</i></li>
<li><b>isMarked()</b> return true if file is marked.</li>
<li><b>copy(newpath)</b> copy file to new location. It return instance of new <i>File<i> object that represent this copied file. This path can be relative or absolute. In case that <i>newpath</i> parameter is relative
<li><b>copy(newpath)</b> copy file to new location. It return instance of new <i>File</i> object that represent this copied file. This path can be relative or absolute. In case that <i>newpath</i> parameter is relative
path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can absolute path. File is then copied to this path. In case that copy fail it return null.</li>
<li><b>move(newpath)</b> move file to new location. It return false if move failed. This can happend if destination is not writable but also if destination file already exist. This functions does not overwrite existing file.
This path can be relative or absolute. In case that <i>newpath</i> parameter is relative path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can be absolute path.
File is then moved to this path.</li>
<li><b>convert(outpath, format, params)</b> convert image file from any format that program is able to open into FITS, XISF, JPEG, PNG, BMP.
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. <i>format</i> one of "FITS" "XISF", "JPG", "PNG" or "BMP". <i>params</i> object with attributes "compressionType" and "compressionLevel".
Valid values for compressionType are be "gzip" or "rice" when converting to FITS. When converting to XISF compressionType can be "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
It is recommended to use "+sh" variants of compression.
XISF format also accept "compressionLevel" in range 0-100 where zero is fastest compression and 100 slowest. If you omit this attribute or set it to -1 then default compression level will be used.
It return new instance of <i>File</i> that point to converted file.
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. This method return new instance of <i>File</i> that point to converted file.
<i>format</i> one of "FITS" "XISF", "JPG", "PNG", "TIFF" or "BMP".
<i>params</i> object with attributes
<ul>
<li>"compressionLevel" used with XISF format. integer value between 0-100 determining speed and compression ratio.</li>
<li>"compressionType" for FITS format it can be "gzip" and "rice". For XISF it can be "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh"</li>
<li>"binning" any integer value above 1 will perform integer downsample</li>
<li>"average" by default set to true. If you set to false it will sum pixel values instead of averaging when performing binning.</li>
<li>"resize" downsample image to defined width and height by subobject <code>"resize":{"width": 128, "height": 128, "aspect":"keep"}</code>
"aspect" determine how to handle aspect ration when resizing image. "keep" and "expand" preserve original aspect ratio. Difference is that "keep" resulting image will be at most requested size
"epand" resulting size will be at least requested size. For example input image 800x600 pixels and resizing to 128x128. With keep resulting image will be 128x96 while with epxand it will be 170x128.
If set to "ignore" then resulting image will be exact size 128x128 ignoring original aspect ratio. By default keep is used.</li>
<li>"autostretch" when set to true it apply automatic stretch function to pixel values. By default it is set to false.</li>
</ul>
In case that both binning and resizing is set binning is performed first then resing.
<pre>file.convert("converted_file.xisf", "xisf", {"compressionType": "zstd+sh", "compressionLevel": 70});
file.convert("converted_file.fits", "fits", {"compressionType": "rice"});
file.convert("converted_file.jpg", "png");</pre>
</li>
<li><b>convertAsync(outpath, format, params)</b> same as previous method but it does conversion in separated thread asynchronously and in parallel. Before calling any method on object returned by this method you must call
<code>core.sync();</code> to ensure that conversion is done and destination file exists.
file.convert("converted_file.jpg", "png");
file.convert("thumbnail.jpg", "jpg", {"binning": 2, "average": true, "resize": {"width":256, "height": 256, "aspect":"ignore"}, "autostretch": true});</pre></li>
<li><b>convertAsync(outpath, format, params)</b> same as previous method but it does conversion in separated thread asynchronously and in parallel.
Before calling any method on object returned by this method you must call <code>core.sync();</code> to ensure that conversion is done and destination file exists.
<pre>let compression = {"compressionType": "zstd+sh"};
let convertedFiles = [];
for(file in files)
@@ -208,11 +329,19 @@ for(file of convertedFiles)// now we can iterate over the files
<li><b>stats()</b> calculate basic images statistics and return them as object with attributes "mean", "stddev", "median", "min", "max" and "mad".
<pre>let s = file.stats();
core.log("Median value is " + s.median);</pre></li>
<li><b>solve(updateHeader)</b> this method will run plate solving on this image and will return solution in form of object with these attributes "ra" and "dec" which are center coordinates of image
"fieldWidth" and "fieldHeight" which is FOV of image in arcseconds, "orientation" is degrees east of north, "pixscale" scale of image in arcseconds per pixel,
"parity" true false value if the image was flipped in vertical direction, "raError" and "decError" deviation from starting point.
When updateHeader is set to true it update FITS header for file with this solution. Default value is false.</li>
<li><b>extractStars(hfr)</b> extract stars will run extraction of stars. When parameter hfr is set to true it will fit HFR on every star.
It return array of objects representing extracted stars. Each object in array
will have these attributes "x" and "y" pixel coordinates of center of star, "mag" relative magnitude of star, "flux" total flux, "peak" peak value of star, "HFR" half flux radius of star,
"a" and "b" semi major and minor axis of star, "theta" angle of orientation of the star, "ra" and "dec" coordinates of star, "numPixels" number of pixel occupied by the star in image.</li>
</ul>
<h4>FITSRecordModify</h4>
This class is used to define modify operation FITS header in FITS and XISF files. It can remove update and add records. Order of operation is also remove then update and last add.
The keyword names may be up to 8 characters long and can only contain uppercase letters, the digits 0-9, the hyphen, and the underscore character.
<p>This class is used to define modify operation FITS header in FITS and XISF files. It can remove update and add records. Order of operation is also remove then update and last add.
The keyword names may be up to 8 characters long and can only contain uppercase letters, the digits 0-9, the hyphen, and the underscore character.</p>
<pre>let modify = new FITSRecordModify();
modify.updateKeyword("OBJECT", "M42");
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
+116 -1
View File
@@ -102,6 +102,63 @@ En appuyant sur la touche Enter ou en cliquant sur le bouton <i>Filtre</i>, les
Cet exemple filtre les fichiers où : "Bias" figure dans le nom de fichier, la propriété OBJECT est "M_42" (où le trait de soulignement peut être n'importe quel caractère) et la propriété DATE commence par "2022".
</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>This module can plate solve images and update FITS header with solution for FITS and XISF images.
<b>Profile</b> this set various parameters that affect star extraction and solving.
<b>Starting point</b> program will try to automatically determine optimal starting point which helps to speed up solving.
You can leave one or both unchecked then it will attempt to do blind solving. If the position or scale is wrong it can actually
fail to solve.
<b>Solution</b> this section contain resulting solution like RA,DEC coordinates center of image, image field of view, orientation as degrees E of N,
image scale in arcseconds per pixel, number of stars extracted and HFR fitting and eccentricity. Then there is log window for debug information from
solver.
</p>
<p>Then finally there are various action button. Settings button show dialog where you can set path to existing index files or auto download some.
Extract button will just extract stars from image and it will show their count, HFR and eccentricity. This action doesn't need index files.
Solve button will try to find coordinates of images. Abort button will stop extraction or solving. Update FITS header will update FITS fits keywords
with found solution.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Traitement par lot</h3>
Ce module permet d'écrire des scripts en JavaScript qui traitent des fichiers images. La fenêtre de traitement par lots se compose de trois parties principales. En haut se trouve la liste des fichiers et répertoires d'entrée.
@@ -133,6 +190,56 @@ Le deuxième paramètre est la valeur par défaut dans la zone de saisie. Les de
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> affiche une boîte de dialogue avec une zone de saisie pour récupérer une valeur décimale. Le texte passé dans le premier argument est utilisé comme label de description.
Le deuxième paramètre est la valeur par défaut dans la zone de saisie. Les trois paramètres sont facultatifs. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
<li><b>getItem(items)</b> affiche une boîte de dialogue de sélection qui permet de sélectionner un élément dans un tableau d'éléments. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
<li><b>setStartingSolution(solution)</b> with this you can set starting point and image scale. It accepth object with attributes "ra", "dec", "pixscale".
Same object as returned by <i>File.solve()</i> method. You can also call it without paramer in which case it will clear any previously set values.</li>
<li><b>getSolverProfile()</b> return solver profile as Object.
<pre>var profile = core.getSolverProfile();
core.log(JSON.stringify(profile));</pre></li>
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
It return button that was clicked as a string.
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
</li>
<li><b>plot(graph)</b> this method show graph defined by JS object.
<pre>
var chart = {
"title": "Chart title", // Title that will show on top of chart
"legend":
{
"visible": true,// default is true
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
},
"series":[ // array of data series
{
"title": "HFR",
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
"y":[2.5,3.1,2.6,2.2] // array of values
},
{
"title": "Ecc",
"y":[0.37, 0.4, 0.35, 0.25],
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
},
{
"title": "Stars",
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
"shape": "star", // shape of markers. valid only for points
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
"y":[523,412,487,510],
"y2": true, // if set to true this serie will use secondary Y axis
"bestFit": true, // show best fit line
"color": "#0000ff"
}
]
};
core.plot(chart);
</pre>
</li>
</ul>
<h4>File</h4>
@@ -159,7 +266,7 @@ le fichier <i>C:/images/lights/red/M42_001.fits</i>, alors cette méthode renver
Ce chemin peut être relatif ou absolu. Dans le cas où le paramètre <i>newpath</i> est un chemin relatif, le "répertoire de sortie" des fenêtres de l'interface graphique est utilisé comme répertoire de base. Le paramètre <i>newpath</i> peut être un chemin absolu.
Le fichier est ensuite déplacé vers ce chemin.</li>
<li><b>convert(outpath, format, params)</b> Convertir un fichier image à partir de n'importe quel format que le programme peut ouvrir en FITS, XISF, JPEG, PNG, BMP.
Les paramètres sont : <i>outputpath</i> chemin où l'image convertie sera enregistrée. Il remplace automatiquement le suffixe en fonction du format. <i>format</i> l'un des éléments suivants : "FITS", "XISF", "JPG", "PNG" ou "BMP". <i>params</i> objet avec les attributs "compressionType" et "compressionLevel".
Les paramètres sont : <i>outputpath</i> chemin où l'image convertie sera enregistrée. Il remplace automatiquement le suffixe en fonction du format. <i>format</i> l'un des éléments suivants : "FITS", "XISF", "JPG", "PNG", "TIFF" ou "BMP". <i>params</i> objet avec les attributs "compressionType" et "compressionLevel".
Les valeurs valides pour compressionType sont "gzip" ou "rice" lors de la conversion en FITS. Lors de la conversion en XISF, compressionType peut être "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
Il est recommandé d'utiliser les variantes de compression "+sh".
Le format XISF accepte également les "compressionLevel" dans la plage 0-100, où zéro est la compression la plus rapide et 100 la plus lente. Si vous omettez cet attribut ou le définissez sur -1, le niveau de compression par défaut sera utilisé.
@@ -185,6 +292,14 @@ for(file of convertedFiles)// now we can iterate over the files
<li><b>stats()</b> calculer les statistiques d'images de base et les renvoyer sous forme d'objet avec des attributs "mean", "stddev", "median", "min", "max" et "mad".
<pre>let s = file.stats();
core.log("Median value is " + s.median);</pre></li>
<li><b>solve(updateHeader)</b> this method will run plate solving on this image and will return solution in form of object with these attributes "ra" and "dec" which are center coordinates of image
"fieldWidth" and "fieldHeight" which is FOV of image in arcseconds, "orientation" is degrees east of north, "pixscale" scale of image in arcseconds per pixel,
"parity" true false value if the image was flipped in vertical direction, "raError" and "decError" deviation from starting point.
When updateHeader is set to true it update FITS header for file with this solution. Default value is false.</li>
<li><b>extractStars(hfr)</b> extract stars will run extraction of stars. When parameter hfr is set to true it will fit HFR on every star.
It return array of objects representing extracted stars. Each object in array
will have these attributes "x" and "y" pixel coordinates of center of star, "mag" relative magnitude of star, "flux" total flux, "peak" peak value of star, "HFR" half flux radius of star,
"a" and "b" semi major and minor axis of star, "theta" angle of orientation of the star, "ra" and "dec" coordinates of star, "numPixels" number of pixel occupied by the star in image.</li>
</ul>
<h4>FITSRecordModify</h4>
+138 -7
View File
@@ -1,5 +1,7 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<title>Pomocník</title>
<style type="text/css">
h1, h2, h3, h4 { padding:0px; margin:10px; }
p { padding:0px; margin:5px 5px 10px 5px; }
@@ -8,21 +10,21 @@ p { padding:0px; margin:5px 5px 10px 5px; }
<body>
<h2>Tenmon pomocník</h2>
<p>Tenmon slúži primárne na zobrazenie astronomických fotiek a obrázkov. Dokáže otvoriť nasledovné formáty:
<p>Tenmon slúži primárne na zobrazenie astronomických fotiek a obrázkov. Dokáže otvoriť nasledovné formáty:</p>
<ul>
<li>FITS 8, 16, 32 bitové celočíselné 32 a 64 bitové s plávajúcou čiarkou</li>
<li>XISF 8, 16, 32 bitové celočíselné 32 a 64 bitové s plávajúcou čiarkou</li>
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM a SVG obrázky</li>
<li>CR2, CR3, NEF, DNG raw obrázky</li>
</ul>
</p>
<h3>Hlavné okno</h3>
<p>V hlavnom okne sa zobrazujú načítané obrázky. Naľavo sú potom <i>Informácie o obrázku</i> kde sa zobrazujú podrobné
informácie o aktuálnom obrázku a <i>Zoznam súborov</i> kde sú všetky obrázky z adresára kde je aktuálne zobrazený obrázok.
Hore je hlavné menu a pod ním je <i>Panel úrovní</i>. Všetky panely sa dajú zavrieť a presúvať. Zatvorený panel sa dá znova
zobraziť v menu <i>Dokovacie panely</i>.
</p>
zobraziť v menu <i>Dokovacie panely</i>.</p>
<p>Na spodnom okraji okna je lišta v ktorej sa ukazuje aktuálna hodnota pixelu pod kurzorom a ak má obrázok WCS dátá aj aktuálne
celestiálne koordináty.</p>
<h3>Otváranie a ukladanie obrázkov</h3>
<p>Otvoriť obrázok je možné v menu <i>Súbor->Otvoriť</i>. Po vybraní súboru ktorý sa má otvoriť je
@@ -43,13 +45,24 @@ hlavné okno na celú obrazovku. <i>Náhľady</i> zobrazí malé náhľady pre v
<p>
Tento panel umožňuje upraviť spôsob ako sa zobrazujú obrazové dáta. Ako prvá je na tomto panely posuvná škála
na ktorej sa dajú nastaviť tri body.
<br><br><img src=":/about/stretch-panel.png">
<br><br><img src=":/about/stretch-panel.png"></p>
<ul>
<li>čierny bod - všetky pixeli s hodnotou menšou ako nastavená budú zobrazené ako čierne</li>
<li>stredný bod - pixeli s touto hodnotou budú zobrazené ako 50% šedá</li>
<li>biely bod - pixeli nad touto hodnotou budú zobrazené ako biele</li>
</ul>
Prvé tlačidlo prepína prepojenie nastavenia čierneho, stretdného a bieleho bodu. Po prepnutí sa dá každý farebný kanál nastaviť
<p>Nasleduje 8 tlačidiel pre nastavenie zobrazenie obrázka:</p>
<ul>
<li><i>Prepojené kanály</i> prepína medzi nataihnutím jasových urovní pre každý RGB kanál zvlášť alebo jednotné pre všetky kanály.</li>
<li><i>Automatické natiahnutie</i> automaticky nastavý čierny, šedý a biely bod pre optimálne zobrazenie..</li>
<li><i>Resetuj funkciu prevodu na obrazovku</i> nastavý hodnoty čierneho, šedého a bieleho bodu na východzie hodnoty.</li>
<li><i>Invertuj farby</i> invertuje zobrazené farby a zobrazý obrázok ako negatív.</li>
<li><i>Falošné farby</i> pre zobrazenie čiernobielych obrázkov sa použije farebná paleta.</li>
<li><i>Preved CFA na farbu</i> Demosaicing black and white image from CFA sensor to color one.</li>
<li><i>Aplikuj automatické natiahnutie pri načítaní</i> pri zapnutí </li>
<li><i>Vykresli equatoriálnu mriežku</i> zapína zobrazenie equatoriálnej mriežky. Je poitrebné aby súbor obsahoval WCS dáta.</li>
</ul>
<p>Prvé tlačidlo prepína prepojenie nastavenia čierneho, stretdného a bieleho bodu. Po prepnutí sa dá každý farebný kanál nastaviť
samostatne.
Nasleduje tlačidlo ktoré nastaví hodnoty čierneho a stredného bodu tak aby bol obrázok zobrazený optimálnym jasom.
Druhé tlačidlo resetuje hodnoty pre čierny, stredný a biely bod na východzie hodnoty. Invertovanie farieb zobrazí obrázok ako negatív.
@@ -92,6 +105,60 @@ V nasledovnom príklade sa vyhľadajú súbory ktoré majú v mene súboru "Bias
zástupný znak za hocijaký reťazec znakov aj žiadny. Znak _ je tiež zástupný znak zastupujúci práve jeden znak.
Bez použitia zástupných znakov sa vyhľadá iba presný výskyt.</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>Tento modul umožnuje vyriešiť obrázok a určiť RA, DEC koordináty a aktualizovať FITS a XISF súbory s WCS dátami.
<b>Profil</b> toto nastavuje rôzne parametre ovplivňujúce extrahovanie hviezd a hľadanie koordinátov.
<b>Štartovný bod</b> program sa pokúsi automaticky určiť bod začiatku hľadania koordinátov pre zrýchlenie hľadania.
Možete nechať jednu alebo obydve voľbi nezaškrtnuté kedy sa bude hľadať riešenie naslepo. Ak je pozícia alebo škála nesprávna hľadanie môže zlyhať.
<b>Riešenie</b> táto časť obsahuje výsledné riešenie hľadania ako sú RA,DEC koordináty stredu obrázku, veľkosť zorného poľa, uhol natočenia v stupňoch,
škála obrázku v arcsekundách na pixel, počet nájdených hviezd a veľkosť hviezd HFR a excentricita. Potom je tu ešte okno so záznamom z riešenia.
</p>
<p>Pod tým je tlačidlo pre nastavenie cesty k indexovým súborom a ich stiahnutie. Je možné buď stiahnuť indexové súbory alebo použiť už existujúce súbory.
Tlačidlo Extrahovať nájde hviezdy a zobrazý ich počet veľkosť HFR a excentricitu. Na toto nie sú potrebné indexové súbory.
Tlačidlo Vyriešiť sa pokúsi nájsť koordináty obrázka. Zrušiť preruší hľadanie riešenia. Aktualizovať FITS hlavičku zapíše najdené riešenie do súboru.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Hromadné spracovanie</h3>
Tento modul umožnuje písanie skriptov v JavaScripte ktoré spracujú súbory obrázkov. Okno Hromadného spracovanie pozostáva z troch častí. Navrchu je zoznam vstupných súborov a adresárov.
@@ -107,6 +174,12 @@ V skripte potom cez toto pole iteruje nasledovne.
}
</pre>
<p>Pod týmto zoznamom je výstupný adresár. Všetky relatívne cesty predané do metod ako sú copy(), move() alebo convert() budú relatívne voči tomuto adresáru.
Ak je ako argument použitá absolútna cesta tak je tento vystupný adresár ignorovaný.</p>
<p>Nasleduje logovacie okno kde sú zapisováné všetký výpisy z behu scriptu. Hlavne volania z <code>core.log()</code> Na spodu je konzola kde je možné vkladať jednoduché príkazy a nakoniec ešte tlačítka
ktoré spúštať alebo zastavovať vybraný skript.</p>
<h4>core</h4>
V skripte je dostupný globálny objekt nazvaný <b>core</b> ktorý má nasledovné metódy.
<ul>
@@ -122,6 +195,56 @@ V skripte je dostupný globálny objekt nazvaný <b>core</b> ktorý má nasledov
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> ukáže diálog pre získanie reálneho čísla. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Tretí parameter je počet desatinných miest.
Obydva parametre sú voliteľné. Vracia zadané číslo alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
<li><b>getItem(items)</b> ukáže dialog pre výber jednej hodnoty z poľa hodnôt. Vracia vybranú hodnotu ako String alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
<li><b>setStartingSolution(solution)</b> with this you can set starting point and image scale. It accepth object with attributes "ra", "dec", "pixscale".
Same object as returned by <i>File.solve()</i> method. You can also call it without paramer in which case it will clear any previously set values.</li>
<li><b>getSolverProfile()</b> return solver profile as Object.
<pre>var profile = core.getSolverProfile();
core.log(JSON.stringify(profile));</pre></li>
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
It return button that was clicked as a string.
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
</li>
<li><b>plot(graph)</b> this method show graph defined by JS object.
<pre>
var chart = {
"title": "Chart title", // Title that will show on top of chart
"legend":
{
"visible": true,// default is true
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
},
"series":[ // array of data series
{
"title": "HFR",
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
"y":[2.5,3.1,2.6,2.2] // array of values
},
{
"title": "Ecc",
"y":[0.37, 0.4, 0.35, 0.25],
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
},
{
"title": "Stars",
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
"shape": "star", // shape of markers. valid only for points
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
"y":[523,412,487,510],
"y2": true, // if set to true this serie will use secondary Y axis
"bestFit": true, // show best fit line
"color": "#0000ff"
}
]
};
core.plot(chart);
</pre>
</li>
</ul>
<h4>File</h4>
@@ -149,7 +272,7 @@ V poli <b>files</b> sú inštancie objektu typu <b>File</b> ktorý ma nasledovn
This path can be relative or absolute. In case that <i>newpath</i> parameter is relative path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can be absolute path.
File is then moved to this path.</li>
<li><b>convert(outpath, format, params)</b> convert image file from any format that program is able to open into FITS, XISF, JPEG, PNG, BMP.
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. <i>format</i> one of "FITS" "XISF", "JPG", "PNG" or "BMP". <i>params</i> object with attributes "compressionType" and "compressionLevel".
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. <i>format</i> one of "FITS" "XISF", "JPG", "PNG", "TIFF" or "BMP". <i>params</i> object with attributes "compressionType" and "compressionLevel".
Valid values for compressionType are be "gzip" or "rice" when converting to FITS. When converting to XISF compressionType can be "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
It is recommended to use "+sh" variants of compression.
XISF format also accept "compressionLevel" in range 0-100 where zero is fastest compression and 100 slowest. If you omit this attribute or set it to -1 then default compression level will be used.
@@ -175,6 +298,14 @@ for(file of convertedFiles)// now we can iterate over the files
<li><b>stats()</b> calculate basic images statistics and return them as object with attributes "mean", "stddev", "median", "min", "max" and "mad".
<pre>let s = file.stats();
core.log("Median value is " + s.median);</pre></li>
<li><b>solve(updateHeader)</b> this method will run plate solving on this image and will return solution in form of object with these attributes "ra" and "dec" which are center coordinates of image
"fieldWidth" and "fieldHeight" which is FOV of image in arcseconds, "orientation" is degrees east of north, "pixscale" scale of image in arcseconds per pixel,
"parity" true false value if the image was flipped in vertical direction, "raError" and "decError" deviation from starting point.
When updateHeader is set to true it update FITS header for file with this solution. Default value is false.</li>
<li><b>extractStars(hfr)</b> extract stars will run extraction of stars. When parameter hfr is set to true it will fit HFR on every star.
It return array of objects representing extracted stars. Each object in array
will have these attributes "x" and "y" pixel coordinates of center of star, "mag" relative magnitude of star, "flux" total flux, "peak" peak value of star, "HFR" half flux radius of star,
"a" and "b" semi major and minor axis of star, "theta" angle of orientation of the star, "ra" and "dec" coordinates of star, "numPixels" number of pixel occupied by the star in image.</li>
</ul>
<h4>FITSRecordModify</h4>
+1 -1
View File
@@ -2,7 +2,7 @@
<td style="padding-right:10px"><img src=":/space.nouspiro.tenmon.png"></td>
<td><h3>Tenmon</h3>
Tenmon is FITS/XISF image viewer and converter. It also index FITS keywords.<br>
v@GITVERSION@ Copyright © 2022 Dušan Poizl<br><br>
v@GITVERSION@ Copyright © 2026 Dušan Poizl<br><br>
This program is free software: you can redistribute it and/or modify<br>
it under the terms of the GNU General Public License as published by<br>
-30
View File
@@ -1,30 +0,0 @@
#ifdef __linux__
#define QT_NO_KEYWORDS
#include <QString>
#include <iostream>
#include <gio/gio.h>
//flatpak bug prevent to use QFile::moveToTrash
bool moveToTrash(const QString &path)
{
GFile *gfile = g_file_new_for_path(path.toLocal8Bit().data());
GError *error = nullptr;
g_file_trash(gfile, nullptr, &error);
if(error)std::cerr << "failed to trash file " << error->code << " " << error->message << std::endl;
g_clear_error(&error);
g_object_unref(gfile);
return true;
}
#else
#include <QFile>
#include <QString>
bool moveToTrash(const QString &path)
{
return QFile::moveToTrash(path);
}
#endif
-286
View File
@@ -1,286 +0,0 @@
#include "imageinfo.h"
#include <QSettings>
#include <QTime>
#include <QHeaderView>
#include <wcslib/wcshdr.h>
#include <wcslib/wcsfix.h>
#include <libxisf.h>
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
bool FITSRecord::editable() const
{
return noEditableKey.count(key);
}
FITSRecord::FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment) :
key(key), value(value), comment(comment)
{
}
FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
{
key = record.name.c_str();
comment = record.comment.c_str();
QString string = record.value.c_str();
if(string.startsWith('\'') && string.endsWith('\''))
{
string.chop(1);
string.remove(0, 1);
}
bool isint;
bool isdouble;
double vald = string.toDouble(&isdouble);
long long vall = string.toLongLong(&isint);
if(isint)
value = vall;
else if(isdouble)
value = vald;
else if(string == "T" || string == "F")
value = string == "T";
else
value = string;
}
FITSRecord::FITSRecord(const LibXISF::Property &property)
{
key = property.id.c_str();
value = QString::fromStdString(property.value.toString());
comment = property.comment.c_str();
xisf = true;
}
QByteArray FITSRecord::valueToByteArray() const
{
if(value.type() == QVariant::Bool)
return value.toBool() ? "T" : "F";
else
return value.toString().toLatin1();
}
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
{
setColumnCount(3);
setHeaderLabels({tr("Property"), tr("Value"), tr("Comment")});
setIndentation(5);
QSettings settings;
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
}
ImageInfo::~ImageInfo()
{
QSettings settings;
settings.setValue("imageinfo/headerstate", header()->saveState());
}
void ImageInfo::setInfo(const ImageInfoData &info)
{
clear();
if(info.fitsHeader.size())
{
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
for(const FITSRecord &record : info.fitsHeader)
{
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
}
addTopLevelItem(fitsHeader);
}
if(info.info.size())
{
QTreeWidgetItem *infoHeader = new QTreeWidgetItem({tr("Image info")});
for(auto &item : info.info)
{
new QTreeWidgetItem(infoHeader, {item.first, item.second});
}
addTopLevelItem(infoHeader);
}
expandAll();
}
void WCSData::freeWCS()
{
wcsvfree(&nwcs, &wcs);
nwcs = 0;
wcs = nullptr;
}
WCSData::WCSData(int width, int height, char *header, int nrec) :
width(width),
height(height)
{
int nreject = 0;
int status = wcspih(header, nrec, 1, 0, &nreject, &nwcs, &wcs);
if(status != 0)
{
freeWCS();
return;
}
status = cdfix(wcs);
if(status > 0 || wcs->crpix[0] == 0)
freeWCS();
}
WCSData::WCSData(int width, int height, const QVector<FITSRecord> &header) :
width(width),
height(height)
{
int status = 0;
QByteArray str;
int nrec = 1;
for(const FITSRecord &record : header)
{
if(record.key.startsWith("PV"))continue;
QByteArray rec;
rec.append(record.key.leftJustified(8, ' '));
rec.append("= ");
rec.append(record.value.toString().toLatin1());
rec.append(" / ");
rec.append(record.comment);
str.append(rec.leftJustified(80, ' ', true));
nrec++;
}
str.append(QByteArray("END").leftJustified(80));
int nreject = 0;
status = wcspih(str.data(), nrec, 1, 0, &nreject, &nwcs, &wcs);
if(status != 0)
{
freeWCS();
return;
}
status = cdfix(wcs);
if(status > 0 || wcs->crpix[0] == 0)
freeWCS();
}
WCSData::~WCSData()
{
if(wcs)
freeWCS();
}
bool WCSData::pixelToWorld(const QPointF &pixel, SkyPoint &point) const
{
if(!valid())return false;
double pixcrd[2] = {pixel.x(), pixel.y()};
double imgcrd[8] = {0};
double phi = 0;
double theta = 0;
double world[8] = {0};
int stat[NWCSFIX] = {0};
int status = wcsp2s(wcs, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
if(status == 0)
{
point = SkyPoint(world[0], world[1]);
return true;
}
return false;
}
bool WCSData::worldToPixel(const SkyPoint &point, QPointF &pixel) const
{
if(!valid())return false;
double world[2] = {point.RA(), point.DEC()};
double phi = 0;
double theta = 0;
double imgcrd[8] = {0};
double pixcrd[8] = {0};
int stat[NWCSFIX] = {0};
int status = wcss2p(wcs, 1, 2, world, &phi, &theta, imgcrd, pixcrd, stat);
if(status == 0)
{
pixel = QPointF(pixcrd[0], pixcrd[1]);
return true;
}
return false;
}
void WCSData::calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const
{
if(wcs == nullptr)return;
minRa = 1000;
maxRa = -1000;
minDec = 1000;
maxDec = -1000;
if(wcs->crval)
{
crVal1 = wcs->crval[0];
crVal2 = wcs->crval[1];
}
else
{
crVal1 = crVal2 = NAN;
}
auto update = [&](const QPointF &pixel)
{
SkyPoint point;
pixelToWorld(pixel, point);
minRa = std::min(minRa, point.RA());
maxRa = std::max(maxRa, point.RA());
minDec = std::min(minDec, point.DEC());
maxDec = std::max(maxDec, point.DEC());
};
for(int x=0; x<width; x++)
{
update(QPointF(x, 0));
update(QPointF(x, height - 1));
}
for(int y=0; y<height; y++)
{
update(QPointF(0, y));
update(QPointF(width - 1, y));
}
QPointF ncp;
QPointF scp;
QRectF s(0, 0, width - 1, height - 1);
if(worldToPixel(SkyPoint(0, 90), ncp))
{
if(s.contains(ncp))
maxDec = 90;
}
if(worldToPixel(SkyPoint(0, -90), scp))
{
if(s.contains(scp))
minDec = -90;
}
}
SkyPoint::SkyPoint() : ra(NAN), dec(NAN)
{
}
SkyPoint::SkyPoint(double ra, double dec) : ra(ra), dec(dec)
{
}
void SkyPoint::set(double ra, double dec)
{
this->ra = ra;
this->dec = dec;
}
QString SkyPoint::toString() const
{
if(std::isnan(ra) || std::isnan(dec))
return QString();
QTime t(0, 0);
t = t.addSecs(ra * 240);
double deg, min, sec;
min = std::abs(std::modf(dec, &deg) * 60);
sec = std::modf(min, &min) * 60;
return QString("RA: %1 DEC: %2° %3' %4\"").arg(t.toString("HH'h' mm'm' ss's'")).arg(deg, 2, 'f', 0, '0').arg(min, 2, 'f', 0, '0').arg(sec, 2, 'f', 0, '0');
}
-83
View File
@@ -1,83 +0,0 @@
#ifndef IMAGEINFO_H
#define IMAGEINFO_H
#include <QTreeWidget>
#include <wcslib/wcs.h>
#include <cmath>
#include <memory>
namespace LibXISF { struct FITSKeyword; struct Property; }
struct FITSRecord
{
QByteArray key;
QVariant value;
QByteArray comment;
bool xisf = false;
bool editable() const;
FITSRecord(){}
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
FITSRecord(const LibXISF::FITSKeyword &record);
FITSRecord(const LibXISF::Property &property);
QByteArray valueToByteArray() const;
};
class SkyPoint
{
double ra = NAN;
double dec = NAN;
public:
SkyPoint();
SkyPoint(double ra, double dec);
void set(double ra, double dec);
double RA() const { return ra; }
double DEC() const { return dec; }
QString toString() const;
};
class WCSData
{
int nwcs = 0;
struct wcsprm *wcs = nullptr;
int width;
int height;
void freeWCS();
public:
WCSData(int width, int height, char *header, int nrec);
WCSData(int width, int height, const QVector<FITSRecord> &header);
WCSData(const WCSData &) = delete;
~WCSData();
bool pixelToWorld(const QPointF &pixel, SkyPoint &point) const;
bool worldToPixel(const SkyPoint &point, QPointF &pixel) const;
void calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const;
bool valid() const { return wcs; };
};
struct ImageInfoData
{
QVector<FITSRecord> fitsHeader;
QVector<QPair<QString, QString>> info;
std::shared_ptr<WCSData> wcs;
};
Q_DECLARE_METATYPE(ImageInfoData);
typedef enum
{
None,
Statistics,
Peaks,
Stars,
}AnalyzeLevel;
class ImageInfo : public QTreeWidget
{
Q_OBJECT
public:
explicit ImageInfo(QWidget *parent);
~ImageInfo() override;
public slots:
void setInfo(const ImageInfoData &info);
};
#endif // IMAGEINFO_H
-121
View File
@@ -1,121 +0,0 @@
#include "imagescrollarea.h"
#include <QMouseEvent>
#include <QScrollBar>
#include <QKeyEvent>
#include <QPalette>
#include <QDebug>
ImageScrollArea::ImageScrollArea(QWidget *parent) : QScrollArea(parent),
m_scale(-1)
{
m_label = new QLabel(this);
setWidget(m_label);
setAlignment(Qt::AlignCenter);
setBackgroundRole(QPalette::Dark);
}
void ImageScrollArea::setImage(const QPixmap &img)
{
m_pixmap = img;
QPixmap pix;
if(m_scale < 0)
pix = img.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
else
pix = img.scaled(img.size() * m_scale, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_label->setPixmap(pix);
m_label->resize(pix.size());
horizontalScrollBar()->setValue(horizontalScrollBar()->maximum() / 2);
verticalScrollBar()->setValue(verticalScrollBar()->maximum() / 2);
}
void ImageScrollArea::setScale(float scale)
{
if(scale > 4 || (scale < 0.2 && scale > 0) || m_pixmap.isNull())
return;
m_scale = scale;
QSize newSize = m_scale < 0 ? size() : m_pixmap.size()*scale;
m_label->setPixmap(m_pixmap.scaled(newSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
m_label->resize(newSize);
}
void ImageScrollArea::zoomIn()
{
if(m_scale < 0)
m_scale = (float)size().width()/m_pixmap.size().width();
setScale(m_scale + 0.1);
}
void ImageScrollArea::zoomOut()
{
if(m_scale < 0)
m_scale = (float)size().width()/m_pixmap.size().width();
setScale(m_scale - 0.1);
}
void ImageScrollArea::bestFit()
{
setScale(-1);
}
void ImageScrollArea::oneToOne()
{
setScale(1);
}
void ImageScrollArea::keyPressEvent(QKeyEvent *event)
{
event->ignore();
}
void ImageScrollArea::keyReleaseEvent(QKeyEvent *event)
{
event->ignore();
}
void ImageScrollArea::mouseMoveEvent(QMouseEvent *event)
{
QPoint delta = m_lastPos - event->pos();
horizontalScrollBar()->setValue(horizontalScrollBar()->value() + delta.x());
verticalScrollBar()->setValue(verticalScrollBar()->value() + delta.y());
m_lastPos = event->pos();
}
void ImageScrollArea::mousePressEvent(QMouseEvent *event)
{
m_lastPos = event->pos();
}
void ImageScrollArea::resizeEvent(QResizeEvent *event)
{
if(m_scale < 0 && !m_pixmap.isNull())
{
m_label->setPixmap(m_pixmap.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
m_label->resize(event->size());
}
QScrollArea::resizeEvent(event);
}
void ImageScrollArea::wheelEvent(QWheelEvent *event)
{
if(m_scale < 0)
m_scale = (float)size().width()/m_pixmap.size().width();
QPointF top(horizontalScrollBar()->value(), verticalScrollBar()->value());
QPointF mousePos = (top + event->position()) / m_scale;
QPoint delta = event->angleDelta();
if(delta.y() > 0)
setScale(m_scale + 0.1);
else
setScale(m_scale - 0.1);
mousePos *= m_scale;
top = mousePos - event->position();
horizontalScrollBar()->setValue(top.x());
verticalScrollBar()->setValue(top.y());
}
-32
View File
@@ -1,32 +0,0 @@
#ifndef IMAGESCROLLAREA_H
#define IMAGESCROLLAREA_H
#include <QScrollArea>
#include <QLabel>
class ImageScrollArea : public QScrollArea
{
Q_OBJECT
QPoint m_lastPos;
QLabel *m_label;
QPixmap m_pixmap;
float m_scale;
public:
explicit ImageScrollArea(QWidget *parent = 0);
void setImage(const QPixmap &img);
void setScale(float scale);
public slots:
void zoomIn();
void zoomOut();
void bestFit();
void oneToOne();
protected:
void keyPressEvent(QKeyEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
};
#endif // IMAGESCROLLAREA_H
+2
View File
@@ -1,5 +1,7 @@
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
find_program(XDG-ICON-RESOURCE_EXECUTABLE xdg-icon-resource)
find_program(XDG-MIME xdg-mime)
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor space.nouspiro.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 resources/space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 resources/space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
execute_process(COMMAND ${XDG-MIME} install --novendor space.nouspiro.tenmon.xisf.xml WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
+1 -1
Submodule libXISF updated: 922d4b73c9...7b70b6a081
-738
View File
@@ -1,738 +0,0 @@
#include "loadrunable.h"
#include "imageringlist.h"
#include <libraw/libraw.h>
#include "imageinfo.h"
#include <QFileInfo>
#include <QPainter>
#include <QElapsedTimer>
#include <QDebug>
#include <iostream>
#include <algorithm>
#include <libexif/exif-data.h>
#include <fitsio2.h>
#include <libxisf.h>
#include "rawimage.h"
#include "starfit.h"
#include "wcslib/wcshdr.h"
#ifdef COLOR_MANAGMENT
#include <QColorSpace>
#endif
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
m_file(file),
m_receiver(receiver),
m_analyzeLevel(level),
m_thumbnail(thumbnail)
{
}
void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag)
{
char val[1024];
ExifEntry *entry = exif_content_get_entry(content, tag);
if(entry)
{
exif_entry_get_value(entry, val, sizeof(val));
info.info.append({exif_tag_get_title(tag), val});
}
}
void drawPeaks(QImage &img, const std::vector<Peak> &peaks)
{
QPixmap pix = QPixmap::fromImage(img);
QPainter painter(&pix);
painter.setPen(Qt::red);
for(auto peak : peaks)
{
painter.drawEllipse(QPoint(peak.x(), peak.y()), 5, 5);
}
img = pix.toImage();
}
void drawStars(QImage &img, const std::vector<Star> &stars)
{
QPixmap pix = QPixmap::fromImage(img);
QPainter painter(&pix);
painter.setPen(Qt::red);
for(auto star : stars)
{
painter.drawEllipse(QPointF(star.m_x, star.m_y), star.hw20X(), star.hw20Y());
}
img = pix.toImage();
}
void printStarModel(int radius, const std::vector<double> &data, const Star &star)
{
QString d = "d=[";
QString m = "m=[";
for(int y=0; y<radius; y++)
{
for(int x=0; x<radius; x++)
{
d += QString::number(data[y*radius+x]) + ",";
m += QString::number(gauss_model(star.m_am, star.m_x, star.m_y, star.m_sx, star.m_sy, x, y)) + ",";
}
d += ";";
m += ";";
}
d += "];";
m += "];";
//std::cout << star.m_am << " " << star.m_sx << star.m_sy << std::endl;
std::cout << d.toStdString() << std::endl;
std::cout << m.toStdString() << std::endl << std::endl;
}
bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
{
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
raw->open_file(path.toLocal8Bit().data());
raw->imgdata.params.half_size = true;
raw->imgdata.params.use_camera_wb = true;
raw->imgdata.params.user_flip = 0;
if(raw->unpack())
return false;
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
size_t size = rawdata.sizes.width*rawdata.sizes.height;
std::vector<uint16_t> out;
out.resize(size);
size_t d = 0;
uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);
for(size_t i=rawdata.sizes.top_margin;i<h;i++)
{
for(size_t o=rawdata.sizes.left_margin;o<w;o++)
{
uint16_t p = rawdata.raw_image[i*pitch+o];
out[d++] = p;
}
}
image = std::make_shared<RawImage>(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16);
memcpy(image->data(), &out[0], sizeof(uint16_t)*d);
QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
if(raw->imgdata.other.shutter < 1)
{
shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
}
info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)});
info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)});
info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
#if LIBRAW_MINOR_VERSION>=19
// info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
#endif
return true;
}
int loadFITSHeader(fitsfile *file, ImageInfoData &info)
{
int imgtype;
int naxis;
long naxes[3] = {0};
int nexist;
int status = 0;
char key[FLEN_KEYWORD];
char val[FLEN_VALUE];
char comm[FLEN_COMMENT];
char strval[FLEN_VALUE];
QVariant var;
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
fits_get_hdrspace(file, &nexist, nullptr, &status);
for(int i=1; i<=nexist; i++)
{
fits_read_keyn(file, i, key, val, comm, &status);
fits_read_key(file, TSTRING, key, strval, nullptr, &status);
if(status == 0 || status == VALUE_UNDEFINED)
{
QString string(strval);
bool isint;
bool isdouble;
double vald = string.toDouble(&isdouble);
long long vall = string.toLongLong(&isint);
if(isint)
var = vall;
else if(isdouble)
var = vald;
else if(status == VALUE_UNDEFINED)
var = QVariant();
else if(string == "T" || string == "F")
var = string == "T";
else
var = string;
status = 0;
info.fitsHeader.append(FITSRecord(key, var, comm));
}
else
{
return status;
}
}
char *header = nullptr;
int nrec = 0;
const char *exclist[] = {"PV1_1", "PV1_2"};
fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
if(status == 0)
{
info.wcs = std::make_shared<WCSData>(naxes[0], naxes[1], header, nrec);
if(!info.wcs->valid())info.wcs.reset();
}
fits_free_memory(header, &status);
return status;
}
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
{
fitsfile *file;
int status = 0;
int type = -1;
fits_open_diskfile(&file, path.toLocal8Bit().data(), READONLY, &status);
int num = 0;
fits_get_num_hdus(file, &num, &status);
int imgtype;
int naxis;
long naxes[3] = {0};
for(int i=1; i <= num; i++)
{
fits_movabs_hdu(file, i, IMAGE_HDU, &status);
fits_get_hdu_type(file, &type, &status);
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
fits_get_img_equivtype(file, &imgtype, &status);
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
{
RawImage::DataType type;
int fitstype;
long fpixel[3] = {1,1,1};
switch(imgtype)
{
case BYTE_IMG:
type = RawImage::UINT8;
fitstype = TBYTE;
break;
case SHORT_IMG:
type = RawImage::UINT16;
fitstype = TSHORT;
break;
case USHORT_IMG:
type = RawImage::UINT16;
fitstype = TUSHORT;
break;
case ULONG_IMG:
type = RawImage::UINT32;
fitstype = TUINT;
break;
case FLOAT_IMG:
type = RawImage::FLOAT32;
fitstype = TFLOAT;
break;
case DOUBLE_IMG:
type = RawImage::FLOAT64;
fitstype = TDOUBLE;
break;
default:
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
goto noload;
break;
}
size_t size = naxes[0]*naxes[1];
size_t w = naxes[0];
size_t h = naxes[1];
info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
info.info.append({QObject::tr("Height"), QString::number(naxes[1])});
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
uint8_t *data = static_cast<uint8_t*>(img.data());
for (int i=1; i==1 || i<=naxes[2]; i++)
{
fpixel[2] = i;
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
}
if(fitstype == TSHORT)
{
uint16_t *s = static_cast<uint16_t*>(img.data());
size_t size = img.size() * img.channels();
for(size_t i=0; i<size; i++)
s[i] -= INT16_MIN;
}
if(img.channels() == 1)
image = std::make_shared<RawImage>(std::move(img));
else
image = RawImage::fromPlanar(img);
if(image)
image->convertToGLFormat();
break;
}
}
noload:
if(file)
loadFITSHeader(file, info);
if(image)
{
for(auto fits : info.fitsHeader)
{
if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP")
image->flip();
}
}
fits_close_file(file, &status);
if(status)
{
char err[100];
fits_get_errstatus(status, err);
info.info.append({QObject::tr("Error"), QString(err)});
qDebug() << "Failed to load FITS file" << err;
}
return true;
}
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
{
try
{
LibXISF::XISFReader xisf;
xisf.open(path.toLocal8Bit().data());
const LibXISF::Image &xisfImage = xisf.getImage(0);
auto fitskeywords = xisfImage.fitsKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append(fits);
}
auto imageproperties = xisfImage.imageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
}
info.wcs = std::make_shared<WCSData>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
if(!info.wcs->valid())info.wcs.reset();
RawImage::DataType type;
switch(xisfImage.sampleFormat())
{
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
default: break;
}
LibXISF::Image tmpImage = xisfImage;
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
{
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
}
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
{
image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
}
if(image)
{
image->convertToGLFormat();
return true;
}
}
catch (LibXISF::Error &err)
{
info.info.append(QPair<QString, QString>("Error", err.what()));
qDebug() << "Failed to load XISF" << err.what();
return false;
}
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
return false;
}
void LoadRunable::run()
{
try
{
if(!m_thumbnail && !m_receiver->isCurrent())
{
return;
}
QElapsedTimer timer;
ImageInfoData info;
QFileInfo finfo(m_file);
info.info.append({QObject::tr("Filename"), finfo.fileName()});
std::shared_ptr<RawImage> rawImage;
if(!loadImage(m_file, info, rawImage))
info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")});
if(rawImage && !m_thumbnail)
{
timer.start();
rawImage->calcStats();
const RawImage::Stats &stats = rawImage->imageStats();
qDebug() << "image stats" << timer.restart();
if(rawImage->channels() == 1)
{
info.info.append({QObject::tr("Mean"), QString::number(stats.m_mean[0])});
info.info.append({QObject::tr("Standart deviation"), QString::number(stats.m_stdDev[0])});
info.info.append({QObject::tr("Median"), QString::number(stats.m_median[0])});
info.info.append({QObject::tr("Minimum"), QString::number(stats.m_min[0])});
info.info.append({QObject::tr("Maximum"), QString::number(stats.m_max[0])});
info.info.append({QObject::tr("MAD"), QString::number(stats.m_mad[0])});
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * stats.m_saturated[0] / rawImage->size()) + "%"});
}
else
{
info.info.append({QObject::tr("Mean"), QString("%1 %2 %3").arg(stats.m_mean[0]).arg(stats.m_mean[1]).arg(stats.m_mean[2])});
info.info.append({QObject::tr("Standart deviation"), QString("%1 %2 %3").arg(stats.m_stdDev[0]).arg(stats.m_stdDev[1]).arg(stats.m_stdDev[2])});
info.info.append({QObject::tr("Median"), QString("%1 %2 %3").arg(stats.m_median[0]).arg(stats.m_median[1]).arg(stats.m_median[2])});
info.info.append({QObject::tr("Minimum"), QString("%1 %2 %3").arg(stats.m_min[0]).arg(stats.m_min[1]).arg(stats.m_min[2])});
info.info.append({QObject::tr("Maximum"), QString("%1 %2 %3").arg(stats.m_max[0]).arg(stats.m_max[1]).arg(stats.m_max[2])});
info.info.append({QObject::tr("MAD"), QString("%1 %2 %3").arg(stats.m_mad[0]).arg(stats.m_mad[1]).arg(stats.m_mad[2])});
info.info.append({QObject::tr("Saturated"), QString("%1 %2 %3%").arg(100.0 * stats.m_saturated[0] / rawImage->size())
.arg(100.0 * stats.m_saturated[1] / rawImage->size())
.arg(100.0 * stats.m_saturated[2] / rawImage->size())});
}
}
if(m_thumbnail)
{
if(rawImage && rawImage->valid())
{
if(QUALITY_RESIZE)
rawImage->resize(THUMB_SIZE, THUMB_SIZE);
rawImage->convertToThumbnail();
}
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
}
else
{
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, info));
}
}
catch(std::exception e)
{
qDebug() << m_file << e.what();
}
}
bool readFITSHeader(const QString &path, ImageInfoData &info)
{
fitsfile *fr;
int status = 0;
fits_open_diskfile(&fr, path.toLocal8Bit().data(), READONLY, &status);
if(fr && status == 0)
{
status = loadFITSHeader(fr, info);
fits_close_file(fr, &status);
}
return status == 0;
}
bool readXISFHeader(const QString &path, ImageInfoData &info)
{
try
{
LibXISF::XISFReader xisf;
xisf.open(path.toLocal8Bit().data());
const LibXISF::Image &image = xisf.getImage(0, false);
auto fitskeywords = image.fitsKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append(fits);
}
auto imageproperties = image.imageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
}
info.wcs = std::make_shared<WCSData>(image.width(), image.height(), info.fitsHeader);
if(!info.wcs->valid())info.wcs.reset();
}
catch (LibXISF::Error &err)
{
qDebug() << err.what();
return false;
}
return true;
}
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage)
{
bool ret = false;
QElapsedTimer timer;
timer.start();
if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
{
ret = loadRAW(path, info, rawImage);
qDebug() << "LoadRAW" << timer.elapsed();
}
else if(path.endsWith(".FIT", Qt::CaseInsensitive) || path.endsWith(".FITS", Qt::CaseInsensitive))
{
ret = loadFITS(path, info, rawImage);
qDebug() << "LoadFITS" << timer.elapsed();
}
else if(path.endsWith(".XISF", Qt::CaseInsensitive))
{
ret = loadXISF(path, info, rawImage);
qDebug() << "LoadXISF" << timer.elapsed();
}
else
{
QImage img(path);
#ifdef COLOR_MANAGMENT
if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb)
img.convertToColorSpace(QColorSpace::SRgb);
#endif
ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData());
info.info.append({QObject::tr("Width"), QString::number(img.width())});
info.info.append({QObject::tr("Height"), QString::number(img.height())});
if(exif)
{
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
exif_data_free(exif);
}
rawImage = std::make_shared<RawImage>(img);
qDebug() << "LoadQImage" << timer.elapsed();
ret = !img.isNull();
}
return ret;
}
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams &params, QSemaphore *semaphore) :
m_infile(in),
m_outfile(out),
m_format(format),
m_params(params),
m_semaphore(semaphore)
{
}
void writeFITSImage(fitsfile *fw, std::shared_ptr<RawImage> rawimage, ImageInfoData &imageinfo)
{
static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"};
int status = 0;
long firstpix[3] = {1,1,1};
int channels = rawimage->channels();
int naxis = channels == 1 ? 2 : 3;
long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->channels()};
std::vector<RawImage> planes;
if(channels == 1)
planes.push_back(*rawimage);
else
planes = rawimage->split();
switch(rawimage->type())
{
case RawImage::UINT8:
fits_create_img(fw, BYTE_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TBYTE, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
case RawImage::UINT16:
fits_create_img(fw, USHORT_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TUSHORT, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
case RawImage::FLOAT32:
fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TFLOAT, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
default:
return;
}
for(const FITSRecord &record : imageinfo.fitsHeader)
{
if(skipKeys.contains(record.key) || record.xisf)continue;
bool isdouble;
bool isint;
bool isbool = record.value.toString() == "T" || record.value.toString() == "F";
double vald = record.value.toDouble(&isdouble);
int valb = record.value.toString() == "T";
long long vall = record.value.toLongLong(&isint);
QByteArray str = record.value.toString().toLatin1();
if(isdouble)
fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(isint)
fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(isbool)
fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(record.key == "COMMENT")
fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(record.key == "HISTORY")
fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else
fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
}
}
void ConvertRunable::run()
{
QSemaphoreReleaser release;
if(m_semaphore)release = QSemaphoreReleaser(m_semaphore);
ImageInfoData imageinfo;
std::shared_ptr<RawImage> rawimage;
loadImage(m_infile, imageinfo, rawimage);
QFileInfo info(m_outfile);
info.dir().mkpath(".");
if(rawimage)
{
if(m_format == "xisf")
{
try
{
LibXISF::XISFWriter xisf;
int channelCount = rawimage->channels();
LibXISF::Image::SampleFormat sampleFormat;
switch(rawimage->type())
{
case RawImage::UINT8: sampleFormat = LibXISF::Image::UInt8; break;
case RawImage::UINT16: sampleFormat = LibXISF::Image::UInt16; break;
case RawImage::FLOAT32: sampleFormat = LibXISF::Image::Float32; break;
default: return;
}
LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Planar);
if(channelCount == 1)
{
std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize());
}
else
{
size_t off = 0;
std::vector<RawImage> planes = rawimage->split();
for(const auto &plane : planes)
{
std::memcpy(image.imageData<uint8_t>() + off, plane.data(), plane.size() * RawImage::typeSize(plane.type()));
off += plane.size() * RawImage::typeSize(plane.type());
}
}
for(auto &record : imageinfo.fitsHeader)
{
if(record.xisf)continue;
if(record.value.typeId() == QMetaType::Bool)
image.addFITSKeyword({record.key.toStdString(), record.value.toBool() ? "T" : "F", record.comment.toStdString()});
else
image.addFITSKeyword({record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});
}
if(m_params.compressionType.startsWith("zstd") && LibXISF::DataBlock::CompressionCodecSupported(LibXISF::DataBlock::ZSTD))
image.setCompression(LibXISF::DataBlock::ZSTD, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("lz4hc"))
image.setCompression(LibXISF::DataBlock::LZ4HC, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("lz4"))
image.setCompression(LibXISF::DataBlock::LZ4, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("zlib"))
image.setCompression(LibXISF::DataBlock::Zlib, m_params.compressionLevel);
if(m_params.compressionType.endsWith("+sh"))
image.setByteshuffling(true);
xisf.writeImage(image);
xisf.save(m_outfile.toLocal8Bit().data());
}
catch(LibXISF::Error &err)
{
qDebug() << "Failed to save XISF image" << err.what();
}
return;
}
if(m_format == "fits")
{
int status = 0;
fitsfile *fw;
if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile);
fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status);
if(!m_params.compressionType.isEmpty())
{
if(m_params.compressionType == "gzip")
fits_set_compression_type(fw, GZIP_1, &status);
else if(m_params.compressionType == "rice")
fits_set_compression_type(fw, RICE_1, &status);
}
writeFITSImage(fw, rawimage, imageinfo);
fits_close_file(fw, &status);
return;
}
// if nothing else try QImage
{
QImage::Format format = QImage::Format_Invalid;
int width = rawimage->widthBytes();
switch(rawimage->type())
{
case RawImage::UINT8:
if(rawimage->channels() == 1)format = QImage::Format_Grayscale8;
else if(rawimage->channels() == 3)format = QImage::Format_RGBX8888;
else if(rawimage->channels() == 4)format = QImage::Format_RGBA8888;
break;
case RawImage::UINT16:
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
width *= 2;
break;
default:
return;
}
if(format == QImage::Format_Invalid)return;
QImage qimage(rawimage->width(), rawimage->height(), format);
for(uint32_t i=0; i < rawimage->height(); i++)
std::memcpy(qimage.scanLine(i), rawimage->data(i), width);
qimage.save(m_outfile);
}
}
}
ConvertRunable::ConvertParams::ConvertParams(const QVariantMap &map)
{
bool ok = false;
if(map.contains("compressionLevel"))
compressionLevel = std::clamp(map["compressionLevel"].toInt(&ok), -1, 100);
if(!ok)compressionLevel = -1;
if(map.contains("compressionType"))
compressionType = map["compressionType"].toString();
}
-37
View File
@@ -1,37 +0,0 @@
#include "mainwindow.h"
#include <QApplication>
#include <QSurfaceFormat>
#include <QTranslator>
#include <stdlib.h>
#include "libxisf.h"
int main(int argc, char *argv[])
{
#ifdef __linux__
setenv("LC_NUMERIC", "C", 1);
#endif
QSurfaceFormat format;
format.setMajorVersion(3);
format.setMinorVersion(3);
//format.setOption(QSurfaceFormat::DebugContext);
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
QApplication a(argc, argv);
a.setOrganizationName("nou");
a.setApplicationName("Tenmon");
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
QTranslator translator;
QTranslator translator2;
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
a.installTranslator(&translator);
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
a.installTranslator(&translator2);
MainWindow w;
w.show();
return a.exec();
}
-792
View File
@@ -1,792 +0,0 @@
#include "rawimage.h"
#include <QDebug>
#include <cstring>
#include <QElapsedTimer>
int THUMB_SIZE = 128;
int THUMB_SIZE_BORDER = 138;
int THUMB_SIZE_BORDER_Y = 158;
double SATURATION = 0.95;
bool QUALITY_RESIZE = true;
#ifdef __SSE2__
template<typename T, int ch>
void fromPlanarSSE(const void *in, void *out, size_t count);
#endif
size_t RawImage::typeSize(RawImage::DataType type)
{
switch(type)
{
case RawImage::UINT8:
return 1;
case RawImage::UINT16:
return 2;
case RawImage::UINT32:
case RawImage::FLOAT32:
return 4;
case RawImage::FLOAT64:
return 8;
default: return 1;
}
}
void RawImage::allocate(uint32_t w, uint32_t h, uint32_t ch, DataType type)
{
m_width = w;
m_height = h;
m_channels = ch;
m_ch = ch == 3 ? 4 : ch;
m_origType = m_type = type;
m_pixels = std::make_unique<PixelType[]>(m_width * m_height * m_ch * typeSize(type));
}
RawImage::RawImage()
{
}
RawImage::RawImage(uint32_t w, uint32_t h, uint32_t ch, DataType type)
{
allocate(w, h, ch, type);
}
RawImage::RawImage(const RawImage &d)
{
allocate(d.m_width, d.m_height, d.m_channels, d.m_type);
std::memcpy(m_pixels.get(), d.m_pixels.get(), m_width * m_height * m_ch * typeSize(m_type));
m_stats = d.m_stats;
}
RawImage::RawImage(RawImage &&d)
{
m_pixels = std::move(d.m_pixels);
m_original = std::move(d.m_original);
m_width = d.m_width;
m_height = d.m_height;
m_channels = d.m_channels;
m_ch = d.m_ch;
m_type = d.m_type;
m_origType = d.m_origType;
m_stats = d.m_stats;
m_thumbAspect = d.m_thumbAspect;
}
RawImage::RawImage(const QImage &img)
{
qDebug() << img;
if(img.format() == QImage::Format_RGBX8888)
{
allocate(img.width(), img.height(), 3, UINT8);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width()*4);
}
else if(img.format() == QImage::Format_RGBA8888)
{
allocate(img.width(), img.height(), 4, UINT8);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width()*4);
}
else if(img.format() == QImage::Format_RGBX64)
{
allocate(img.width(), img.height(), 3, UINT16);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width()*8);
}
else if(img.format() == QImage::Format_RGBA64)
{
allocate(img.width(), img.height(), 4, UINT16);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width()*8);
}
else if(img.format() == QImage::Format_Grayscale8)
{
allocate(img.width(), img.height(), 1, UINT8);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width());
}
else if(img.format() == QImage::Format_Grayscale16)
{
allocate(img.width(), img.height(), 1, UINT16);
for(int i=0; i<img.height(); i++)
std::memcpy(data(i), img.scanLine(i), img.width()*2);
}
else
{
QImage tmp = img.convertToFormat(QImage::Format_RGBA8888);
allocate(img.width(), img.height(), 4, UINT8);
for(int i=0; i<tmp.height(); i++)
std::memcpy(data(i), tmp.scanLine(i), tmp.width()*4);
}
m_stats.m_stats = false;
}
const RawImage::Stats& RawImage::imageStats() const
{
return m_stats;
}
template<typename T, typename U, int ch>
void calcStats(const T *data, size_t n, size_t w, RawImage::Stats &stats)
{
U sum[4] = {0};
U sumSq[4] = {0};
T min[4] = {std::numeric_limits<T>::max(), std::numeric_limits<T>::max(), std::numeric_limits<T>::max(), std::numeric_limits<T>::max()};
T max[4] = {std::numeric_limits<T>::min(), std::numeric_limits<T>::min(), std::numeric_limits<T>::min(), std::numeric_limits<T>::min()};
uint32_t histSize = 65536;
if constexpr(std::is_same<T, uint8_t>::value)histSize = 256;
std::vector<uint32_t> histogram[4];
histogram[0].resize(histSize); histogram[1].resize(histSize); histogram[2].resize(histSize); histogram[3].resize(histSize);
T sat = SATURATION * std::numeric_limits<T>::max();
if constexpr(!std::numeric_limits<T>::is_integer)sat = SATURATION;
uint32_t saturated[4] = {0};
auto statsFunc = [&](T d, int x)
{
sum[x] += d;
sumSq[x] += (U)d * d;
min[x] = std::min(min[x], d);
max[x] = std::max(max[x], d);
uint16_t idx;
if constexpr(std::is_same<T, uint32_t>::value)idx = d >> 16;
if constexpr(std::is_same<T, uint8_t>::value || std::is_same<T, uint16_t>::value)idx = d;
if constexpr(!std::numeric_limits<T>::is_integer)idx = std::clamp((T)d * histSize, (T)0.0, (T)65535.0);
histogram[x][idx]++;
if(d > sat)saturated[x]++;
};
auto findMedian = [histSize](std::vector<uint32_t> &histogram, size_t n) -> size_t
{
size_t histSum = 0;
for(size_t o=0; o < histSize; o++)
{
histSum += histogram[o];
if(histSum >= n/2)
return o;
}
return 0;
};
size_t na[4] = {n, n, n, n};
if constexpr(ch == 1)
{
na[1] /= 4;
na[2] /= 2;
na[3] /= 4;
}
for(size_t i = 0; i < n; i++)
{
statsFunc(data[i*ch], 0);
if constexpr(ch >= 3)
{
statsFunc(data[i*ch + 1], 1);
statsFunc(data[i*ch + 2], 2);
}
}
if constexpr(ch == 1)
{
size_t h = (n / w) & (SIZE_MAX-1);
w &= (SIZE_MAX-1);
for(size_t y=0; y<h; y+=2)
{
for(size_t x=0; x<w; x+=2)
{
statsFunc(data[y*w+x], 1);
statsFunc(data[y*w+x+1], 2);
statsFunc(data[(y+1)*w+x], 2);
statsFunc(data[(y+1)*w+x+1], 3);
}
}
}
for(int i = 0; i < 4; i++)
{
stats.m_min[i] = min[i];
stats.m_max[i] = max[i];
stats.m_mean[i] = (double)sum[i] / na[i];
stats.m_saturated[i] = saturated[i];
double sum2 = (double)sum[i] * sum[i];
stats.m_stdDev[i] = std::sqrt((sumSq[i] - sum2 / na[i]) / (na[i] - 1));
uint32_t median = findMedian(histogram[i], na[i]);
stats.m_median[i] = median;
std::vector<uint32_t> madHist(histSize, 0);
madHist[0] = histogram[i][median];
for(size_t o = 1; o < histSize; o++)
{
if(median + o < histSize)madHist[o] += histogram[i][median + o];
if(o <= median)madHist[o] += histogram[i][median - o];
}
stats.m_mad[i] = findMedian(madHist, na[i]);
if constexpr(!std::numeric_limits<T>::is_integer)
{
stats.m_median[i] /= 65535.0;
stats.m_mad[i] /= 65535.0;
}
}
stats.m_histogram.resize(histSize, 0);
for(size_t i = 0; i < histSize; i++)
for(size_t o = 0; o < ch; o++)
stats.m_histogram[i] += histogram[o][i];
}
void RawImage::calcStats()
{
if(m_stats.m_stats)return;
m_stats.m_stats = true;
switch(m_origType)
{
case UINT8:
if(channels()==1)
::calcStats<uint8_t, uint64_t, 1>(static_cast<const uint8_t*>(origData()), size(), width(), m_stats);
else
::calcStats<uint8_t, uint64_t, 4>(static_cast<const uint8_t*>(origData()), size(), width(), m_stats);
break;
case UINT16:
if(channels()==1)
::calcStats<uint16_t, uint64_t, 1>(static_cast<const uint16_t*>(origData()), size(), width(), m_stats);
else
::calcStats<uint16_t, uint64_t, 4>(static_cast<const uint16_t*>(origData()), size(), width(), m_stats);
break;
case UINT32:
if(channels()==1)
::calcStats<uint32_t, double, 1>(static_cast<const uint32_t*>(origData()), size(), width(), m_stats);
else
::calcStats<uint32_t, double, 4>(static_cast<const uint32_t*>(origData()), size(), width(), m_stats);
break;
case FLOAT32:
if(channels()==1)
::calcStats<float, double, 1>(static_cast<const float*>(origData()), size(), width(), m_stats);
else
::calcStats<float, double, 4>(static_cast<const float*>(origData()), size(), width(), m_stats);
break;
case FLOAT64:
if(channels()==1)
::calcStats<double, double, 1>(static_cast<const double*>(origData()), size(), width(), m_stats);
else
::calcStats<double, double, 4>(static_cast<const double*>(origData()), size(), width(), m_stats);
break;
}
}
void RawImage::rect(int &x, int &y, int w, int h, std::vector<double> &r) const
{
/*r.resize(w*h);
x -= w/2;
y -= h/2;
if(x<0)x = 0;
if(y<0)y = 0;
if(x+w >= m_img.cols)x = m_img.cols-w;
if(y+h >= m_img.rows)y = m_img.rows-h;
cv::Mat roiImg(m_img, cv::Rect(x, y, w, h));
cv::Mat doubleMat;
roiImg.convertTo(doubleMat, CV_64F);
r = std::vector<double>(doubleMat.begin<double>(), doubleMat.end<double>());*/
}
int RawImage::findPeaks(double background, double distance, std::vector<Peak> &peaks) const
{
/*std::vector<std::vector<cv::Point>> contours;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(distance, distance));
cv::Mat img, mask, dilate, locMax, result;
if(m_img.channels() == 1)img = m_img;
else cv::cvtColor(m_img, img, cv::COLOR_RGB2GRAY);
cv::dilate(img, dilate, kernel);
cv::compare(img, dilate, locMax, cv::CMP_GE);
cv::compare(img, cv::Scalar(background), mask, cv::CMP_GT);
cv::bitwise_and(locMax, mask, result);
cv::findContours(result, contours, cv::noArray(), cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
peaks.reserve(contours.size());
for(auto contour : contours)
{
peaks.push_back(Peak(1, contour[0].x, contour[0].y));
}
return peaks.size();*/
}
uint32_t RawImage::width() const
{
return m_width;
}
uint32_t RawImage::height() const
{
return m_height;
}
uint32_t RawImage::channels() const
{
return m_channels;
}
uint32_t RawImage::size() const
{
return width()*height();
}
RawImage::DataType RawImage::type() const
{
return m_type;
}
uint32_t RawImage::norm() const
{
switch(m_type)
{
case UINT8:
return UINT8_MAX;
case UINT16:
return UINT16_MAX;
case UINT32:
return UINT32_MAX;
default:
return 1;
}
}
uint32_t RawImage::widthBytes() const
{
return m_ch * m_width;
}
void* RawImage::data()
{
return m_pixels.get();
}
const void *RawImage::data() const
{
return m_pixels.get();
}
void *RawImage::data(uint32_t row, uint32_t col)
{
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
}
const void *RawImage::data(uint32_t row, uint32_t col) const
{
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
}
const void *RawImage::origData() const
{
if(m_original)
return m_original.get();
else
return m_pixels.get();
}
const void *RawImage::origData(uint32_t row, uint32_t col) const
{
if(m_original)
return m_original.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_origType);
else
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
}
bool RawImage::planar() const
{
return m_planar;
}
void RawImage::convertToThumbnail()
{
if(!valid())
return;
if(m_thumbAspect == 0.0f)
m_thumbAspect = (float)width() / height();
std::unique_ptr<PixelType[]> outptr = std::make_unique<PixelType[]>(THUMB_SIZE * THUMB_SIZE * 4 * sizeof(uint16_t));
uint16_t *out = reinterpret_cast<uint16_t*>(outptr.get());
auto loop = [&](uint16_t *out, auto *in, auto scale)
{
for(int i=0; i<THUMB_SIZE; i++)
{
for(int o=0; o<THUMB_SIZE; o++)
{
int idx = (i*THUMB_SIZE + o)*4;
int idx2 = ((i * m_height / THUMB_SIZE * m_width) + (o * m_width / THUMB_SIZE)) * m_ch;
if(m_channels == 1)
{
out[idx] = out[idx + 1] = out[idx + 2] = in[idx2] * scale;
}
else
{
out[idx] = in[idx2] * scale;;
out[idx + 1] = in[idx2 + 1] * scale;;
out[idx + 2] = in[idx2 + 2] * scale;;
}
out[idx + 3] = UINT16_MAX;
}
}
};
switch(m_type)
{
case UINT8:
loop(out, reinterpret_cast<uint8_t*>(m_pixels.get()), 256);
break;
case UINT16:
loop(out, reinterpret_cast<uint16_t*>(m_pixels.get()), 1);
break;
case UINT32:
loop(out, reinterpret_cast<uint32_t*>(m_pixels.get()), UINT16_MAX/(float)UINT32_MAX);
break;
case FLOAT32:
loop(out, reinterpret_cast<float*>(m_pixels.get()), 65535.0);
break;
default:
qWarning() << "FLOAT64 should not happend";
return;
}
m_pixels = std::move(outptr);
m_width = THUMB_SIZE;
m_height = THUMB_SIZE;
m_ch = 4;
m_channels = 3;
m_type = UINT16;
}
void RawImage::convertToGLFormat()
{
size_t s = size() * m_ch;
if(m_type == UINT32)
{
m_original = std::move(m_pixels);
allocate(m_width, m_height, m_channels, FLOAT32);
m_origType = UINT32;
float *dst = reinterpret_cast<float*>(m_pixels.get());
uint32_t *src = reinterpret_cast<uint32_t*>(m_original.get());
for(size_t i = 0; i < s; i++)
dst[i] = src[i] / (float)UINT32_MAX;
}
else if(m_type == FLOAT64)
{
m_original = std::move(m_pixels);
allocate(m_width, m_height, m_channels, FLOAT32);
m_origType = FLOAT64;
float *dst = reinterpret_cast<float*>(m_pixels.get());
double *src = reinterpret_cast<double*>(m_original.get());
for(size_t i = 0; i < s; i++)
dst[i] = src[i];
}
}
float RawImage::thumbAspect() const
{
return m_thumbAspect;
}
bool RawImage::pixel(int x, int y, double &r, double &g, double &b) const
{
if(x < 0 || y < 0 || x >= (int)width() || y >= (int)height())return false;
switch(m_origType)
{
case UINT8:
{
const uint8_t *v = static_cast<const uint8_t*>(origData(y, x));
if(m_channels == 1)
{
r = g = b = *v;
}
else
{
r = v[0];
g = v[1];
b = v[2];
}
break;
}
case UINT16:
{
const uint16_t *v = static_cast<const uint16_t*>(origData(y, x));
if(m_channels == 1)
{
r = g = b = *v;
}
else
{
r = v[0];
g = v[1];
b = v[2];
}
break;
}
case UINT32:
{
const uint32_t *v = static_cast<const uint32_t*>(origData(y, x));
if(m_channels == 1)
{
r = g = b = *v;
}
else
{
r = v[0];
g = v[1];
b = v[2];
}
break;
}
case FLOAT32:
{
const float *v = static_cast<const float*>(origData(y, x));
if(m_channels == 1)
{
r = g = b = *v;
}
else
{
r = v[0];
g = v[1];
b = v[2];
}
break;
}
case FLOAT64:
{
const double *v = static_cast<const double*>(origData(y, x));
if(m_channels == 1)
{
r = g = b = *v;
}
else
{
r = v[0];
g = v[1];
b = v[2];
}
break;
}
}
return true;
}
template<typename T>
void boxResample(uint32_t w, uint32_t h, uint32_t ch, uint32_t oldw, uint32_t oldh, const uint8_t *in_, uint8_t *out_)
{
if(oldw == 0 || oldh == 0)return;
const T *in = reinterpret_cast<const T*>(in_);
T *out = reinterpret_cast<T*>(out_);
float max = 255.0f;
if constexpr(std::is_same<T, uint16_t>::value)
max = UINT16_MAX;
float sx = (float)w / oldw;
float sy = (float)h / oldh;
for(uint32_t y = 0; y < h; y++)//iterate over destination Y
{
for(uint32_t x = 0; x < w; x++)//iterate over destination X
{
float p[4] = {0.0f};
uint32_t xx = x * oldw / w;//calculate source rect
uint32_t yy = y * oldh / h;
uint32_t xe = std::min((x + 1) * oldw / w, oldw - 1);
uint32_t ye = std::min((y + 1) * oldh / h, oldh - 1);
for(uint32_t o = yy; o <= ye; o++)//iterate over source Y
{
float cy = o * sy - y;
if(cy < 0.0f)cy = sy + cy;
else if(sy + cy > 1.0f)cy = 1.0f - cy;
else cy = sy;
if(yy==ye)cy = 1.0f;
for(uint32_t i = xx; i <= xe; i++)//iterate over source X
{
float cx = i * sx - x;
if(cx < 0.0f)cx = sx + cx;
else if(sx + cx > 1.0f)cx = 1.0f - cx;
else cx = sx;
if(xx==xe)cx = 1.0f;
for(uint32_t z = 0; z < ch; z++)
p[z] += in[(o * oldw + i) * ch + z] * cy * cx;
}
}
for(uint32_t z = 0; z < ch; z++)
if constexpr(std::is_floating_point<T>::value)
out[(y * w + x) * ch + z] = p[z];
else
out[(y * w + x) * ch + z] = std::clamp(std::round(p[z]), 0.0f, max);
}
}
}
void RawImage::resize(uint32_t w, uint32_t h)
{
if(!valid())return;
std::unique_ptr<PixelType[]> old_pixels = std::move(m_pixels);
uint32_t oldw = m_width;
uint32_t oldh = m_height;
m_thumbAspect = (float)m_width / m_height;
allocate(w, h, m_channels, m_type);
switch(m_type)
{
case RawImage::UINT8:
boxResample<uint8_t>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
break;
case RawImage::UINT16:
boxResample<uint16_t>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
break;
case RawImage::FLOAT32:
boxResample<float>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
break;
default:
qWarning() << "Resizing format not supported";
break;
}
}
std::pair<float, float> RawImage::unitScale() const
{
float min = *std::min_element(m_stats.m_min, m_stats.m_min + 4);
float max = *std::max_element(m_stats.m_max, m_stats.m_max + 4);
if(m_origType == UINT32)
{
min /= (float)UINT32_MAX;
max /= (float)UINT32_MAX;
}
if(min < 0.0f || max > 1.0f)
return {1.0f / (max - min), min / (max - min)};
else
return {1.0f, 0.0f};
}
void RawImage::flip()
{
std::unique_ptr<PixelType[]> tmp = std::move(m_pixels);
allocate(m_width, m_height, m_channels, m_type);
uint32_t rowSize = m_width * m_ch * typeSize(m_type);
for(uint32_t i=0; i<m_height; i++)
std::memcpy(m_pixels.get() + rowSize * (m_height - i - 1), tmp.get() + rowSize * i, rowSize);
}
std::shared_ptr<RawImage> RawImage::fromPlanar(const RawImage &img)
{
return RawImage::fromPlanar(img.data(), img.width(), img.height(), img.channels(), img.type());
}
std::shared_ptr<RawImage> RawImage::fromPlanar(const void *pixels, uint32_t w, uint32_t h, uint32_t ch, RawImage::DataType type)
{
std::shared_ptr<RawImage> image = std::make_shared<RawImage>(w, h, ch, type);
size_t size = w * h;
size_t ch2 = ch == 1 ? 1 : 4;
auto convert = [&](auto *in, auto *out, auto alpha)
{
for(size_t i=0; i<size; i++)
for(size_t o=0; o<ch; o++)
out[i*ch2 + o] = in[o*size + i];
if(ch != ch2)
for(size_t i=0; i<size; i++)
out[i*ch2 + 3] = alpha;
};
switch(type)
{
case UINT8:
#ifdef __SSE2__
if(ch==3)
fromPlanarSSE<uint8_t, 3>(pixels, image->data(), size);
else
fromPlanarSSE<uint8_t, 4>(pixels, image->data(), size);
#else
convert(static_cast<const uint8_t*>(pixels), static_cast<uint8_t*>(image->data()), UINT8_MAX);
#endif
break;
case UINT16:
#ifdef __SSE2__
if(ch==3)
fromPlanarSSE<uint16_t, 3>(pixels, static_cast<uint16_t*>(image->data()), size);
else
fromPlanarSSE<uint16_t, 4>(pixels, static_cast<uint16_t*>(image->data()), size);
#else
convert(static_cast<const uint16_t*>(pixels), static_cast<uint16_t*>(image->data()), UINT16_MAX);
#endif
break;
case UINT32:
#ifdef __SSE2__
if(ch==3)
fromPlanarSSE<uint32_t, 3>(pixels, image->data(), size);
else
fromPlanarSSE<uint32_t, 4>(pixels, image->data(), size);
#else
convert(static_cast<const uint32_t*>(pixels), static_cast<uint32_t*>(image->data()), UINT32_MAX);
#endif
break;
case FLOAT32:
#ifdef __SSE2__
if(ch==3)
fromPlanarSSE<float, 3>(pixels, image->data(), size);
else
fromPlanarSSE<float, 4>(pixels, image->data(), size);
#else
convert(static_cast<const float*>(pixels), static_cast<float*>(image->data()), 1);
#endif
break;
case FLOAT64:
convert(static_cast<const double*>(pixels), static_cast<double*>(image->data()), 1);
break;
}
image->m_planar = false;
return image;
}
std::vector<RawImage> RawImage::split() const
{
std::vector<RawImage> planes;
planes.resize(m_channels);
for(size_t i=0; i<m_channels; i++)
planes[i].allocate(m_width, m_height, 1, m_type);
size_t s = size();
auto extract = [&](auto *in, auto *out, size_t off)
{
for(size_t i=0; i < s; i++)
out[i] = in[i*m_ch + off];
};
for(uint32_t i=0; i<m_channels; i++)
{
switch(m_type)
{
case UINT8:
extract(static_cast<const uint8_t*>(data()), static_cast<uint8_t*>(planes[i].data()), i);
break;
case UINT16:
extract(static_cast<const uint16_t*>(data()), static_cast<uint16_t*>(planes[i].data()), i);
break;
case UINT32:
case FLOAT32:
extract(static_cast<const uint32_t*>(data()), static_cast<uint32_t*>(planes[i].data()), i);
break;
case FLOAT64:
extract(static_cast<const double*>(data()), static_cast<double*>(planes[i].data()), i);
break;
}
}
return planes;
}
bool RawImage::valid() const
{
return m_width > 0 && m_height > 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.699999 12.699999"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="grid.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="9.5144352"
inkscape:cx="39.361243"
inkscape:cy="25.067174"
inkscape:window-width="2510"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<inkscape:path-effect
effect="skeletal"
id="path-effect636"
is_visible="true"
lpeversion="1"
pattern="M 0,4.992138 C 0,2.2364778 2.2364778,0 4.992138,0 c 2.7556601,0 4.9921379,2.2364778 4.9921379,4.992138 0,2.7556601 -2.2364778,4.9921379 -4.9921379,4.9921379 C 2.2364778,9.9842759 0,7.7477981 0,4.992138 Z"
copytype="single_stretched"
prop_scale="1"
scale_y_rel="false"
spacing="0"
normal_offset="0"
tang_offset="0"
prop_units="false"
vertical_pattern="false"
hide_knot="false"
fuse_tolerance="0" />
<inkscape:path-effect
effect="skeletal"
id="path-effect632"
is_visible="true"
lpeversion="1"
pattern="M 0,4.992138 C 0,2.2364778 2.2364778,0 4.992138,0 c 2.7556601,0 4.9921379,2.2364778 4.9921379,4.992138 0,2.7556601 -2.2364778,4.9921379 -4.9921379,4.9921379 C 2.2364778,9.9842759 0,7.7477981 0,4.992138 Z"
copytype="single_stretched"
prop_scale="1"
scale_y_rel="false"
spacing="0"
normal_offset="0"
tang_offset="0"
prop_units="false"
vertical_pattern="false"
hide_knot="false"
fuse_tolerance="0" />
<inkscape:path-effect
effect="skeletal"
id="path-effect628"
is_visible="true"
lpeversion="1"
pattern="M 0,4.992138 C 0,2.2364778 2.2364778,0 4.992138,0 c 2.7556601,0 4.9921379,2.2364778 4.9921379,4.992138 0,2.7556601 -2.2364778,4.9921379 -4.9921379,4.9921379 C 2.2364778,9.9842759 0,7.7477981 0,4.992138 Z"
copytype="single_stretched"
prop_scale="1"
scale_y_rel="false"
spacing="0"
normal_offset="0"
tang_offset="0"
prop_units="false"
vertical_pattern="false"
hide_knot="false"
fuse_tolerance="0" />
<inkscape:path-effect
effect="bspline"
id="path-effect624"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="spiro"
id="path-effect620"
is_visible="true"
lpeversion="1" />
</defs>
<g
inkscape:label="Vrstva 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;stroke:#000000;stroke-width:0.503;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
d="M -5,-5 13,13"
id="path616"
sodipodi:nodetypes="cc" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.503;stroke-linejoin:round;stroke-opacity:1;stroke-dasharray:none"
id="path643"
cx="-4.9824347"
cy="-4.9865055"
r="12.973718" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.503;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path1665"
cx="-4.9600825"
cy="-4.9741392"
r="17.086035" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.503;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path1667"
cx="-5.0079365"
cy="-5.0034046"
r="21.147657" />
<path
style="fill:none;stroke:#000000;stroke-width:0.467;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
d="M 14.371451,3.5622727 -4.9904999,-5.0054782 4.2432806,13.903978"
id="path1734" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.503;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path1736"
cx="-5.155458"
cy="-5.1256938"
r="9.6808758" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
View File
Binary file not shown.
+6 -1
View File
@@ -3,7 +3,6 @@
<file>invert.png</file>
<file>nuke.png</file>
<file>bayer.png</file>
<file>space.nouspiro.tenmon.png</file>
<file>nuke_a.png</file>
<file>../about/tenmon</file>
<file>../translations/tenmon_en.qm</file>
@@ -16,6 +15,12 @@
<file>bggr.png</file>
<file>grbg.png</file>
<file>gbrg.png</file>
<file>space.nouspiro.tenmon.png</file>
<file>../translations/tenmon_pt_BR.qm</file>
<file alias="help">../about/help_en</file>
<file>colormap.png</file>
<file>ngc.db</file>
<file>grid.svg</file>
</qresource>
<qresource lang="en" prefix="/">
<file alias="help">../about/help_en</file>
+12
View File
@@ -0,0 +1,12 @@
core.log("This script convert any FITS file into XISF with ZSTD compression");
let compression = {"compressionType": "zstd+sh"};
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit")
{
core.log("Converting " + file.fileName());
file.convertAsync(file.relativeFilePath(), "XISF", compression);
}
}
+36
View File
@@ -0,0 +1,36 @@
// how to get input from user
let d = core.getFloat("Getting float value");
let i = core.getInt("Getting integer value");
let s = core.getString("Getting string value");
// print user input
core.log("Your input " + d + " " + i + " " + s);
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
{
let keywords = file.fitsKeywords();
let item = core.getItem(keywords, "Select keyword");
core.log("You selected keyword " + item); core.log(file.fitsKeywords());
core.log("fileName() " + file.fileName());
core.log("absoluteFilePath() " + file.absoluteFilePath());
core.log("absolutePath() " + file.absolutePath());
core.log("relativeFilePath() " + file.relativeFilePath());
core.log("relativePath() " + file.relativePath());
core.log("baseName() " + file.baseName());
core.log("completeBase() " + file.completeBaseName());
core.log("suffix() " + file.suffix());
core.log("size() " + file.size());
let stats = file.stats();
core.log("Image statistics");
core.log("\tMinimum: " + stats.min);
core.log("\tMaximum: " + stats.max);
core.log("\tMedian:" + stats.median);
core.log("\tStandard deviation:" + stats.stddev);
core.log("\tMedian Absolute Deviation:" + stats.mad);
break;
}
}
+49
View File
@@ -0,0 +1,49 @@
core.log("Measure HFR and eccentricity of stars");
var chart = {
"title": "Measure stars",
"legend": {"visible": true, "align": "left"},
"series": [
{
"title": "HFR",
"type": "bar",
"y":[]
},
{
"title": "Ecc",
"type": "bar",
"y":[]
},
{
"title": "Star count",
"type": "linePoints",
"y":[],
"y2": true,
"bestFit": true
}
]
};
core.setSolverProfile(5);
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
{
var stars = file.extractStars(true);
var sumHFR = 0;
var ecc = 0;
for(star of stars)
{
sumHFR += star.HFR;
ecc += Math.sqrt(1 - (star.b * star.b) / (star.a * star.a));
}
chart.series[0].y.push(sumHFR / stars.length);
chart.series[1].y.push(ecc / stars.length);
chart.series[2].y.push(stars.length);
core.log(file.fileName() + " Stars:" + stars.length + " HFR: " + sumHFR / stars.length + " Ecc: " + ecc / stars.length);
}
}
core.plot(chart);
+8
View File
@@ -0,0 +1,8 @@
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
{
let stats = file.stats();
core.log("File: \"" + file.fileName() + "\" - median: " + stats.median);
}
}
+85
View File
@@ -0,0 +1,85 @@
core.log("Script to modify FITS header in FITS and XISF files");
function checkFITS(key)
{
const noEditableKey = ["SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"];
return noEditableKey.indexOf(key) < 0;
}
if(files.length == 0)
{
core.log("No input files");
throw "";
}
let action = core.getItem(["UPDATE", "UPDATE_ADD", "ADD", "REMOVE"], "Do you want update, add or remove record?");
let modify = new FITSRecordModify();
let proceed = false;
if(action == "UPDATE")
{
let keywords = files[0].fitsKeywords().filter(checkFITS);
let keyword = core.getItem(keywords, "Select keyword to update");
let value = files[0].fitsValue(keyword);
if(isNaN(value))
value = core.getString("Enter new value", value);
else
value = core.getFloat("Enter new value", value);
if(keyword && value)
{
proceed = true;
modify.updateKeyword(keyword, value);
}
}
else if(action == "UPDATE_ADD")
{
let keyword = core.getString("Enter keyword to update");
let value = core.getString("Enter new value");
if(keyword && value)
{
proceed = true;
keyword = keyword.toUpperCase();
modify.updateKeyword(keyword, value);
}
}
else if(action == "ADD")
{
let keyword = core.getString("Enter keyword to add");
let value = core.getString("Enter new value");
if(keyword && value)
{
proceed = true;
keyword = keyword.toUpperCase();
modify.addKeyword(keyword, value);
}
}
else if(action == "REMOVE")
{
let keywords = files[0].fitsKeywords().filter(checkFITS);
let keyword = core.getItem(keywords, "Select keyword to remove");
if(keyword)
{
proceed = true;
modify.removeKeyword(keyword);
}
}
if(proceed)
{
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
{
core.log("Modifing " + file.fileName());
file.modifyFITSRecords(modify);
}
}
}
else
{
core.log("Canceled");
}
+20
View File
@@ -0,0 +1,20 @@
core.log("Plate solve and update solution");
var first = true;
var update = core.question("Update FITS header with solution?", ["yes", "no"], "Update FITS header") == "yes";
var blind = core.question("Do blind solve every image?", ["yes", "no"], "Blind solve?") == "yes";
for(file of files)
{
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
{
var solution = file.solve(update);
if(first && !blind)
{
core.setStartingSolution(solution);
first = false;
}
core.log(file.fileName() + " " + "RA: " + (solution.ra / 15) + "h DEC: " + solution.dec + "deg");
}
}
+10
View File
@@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/scripts">
<file>example script</file>
<file>convert to XISF</file>
<file>median</file>
<file>modify FITS header</file>
<file>measure HFR</file>
<file>plate solve</file>
</qresource>
</RCC>
-2
View File
@@ -1,5 +1,3 @@
#version 330
uniform sampler2D qt_Texture0;
uniform ivec2 firstRed;
in vec2 qt_TexCoord0;
-2
View File
@@ -1,5 +1,3 @@
#version 330
uniform sampler2D qt_Texture0;
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
+17 -17
View File
@@ -1,6 +1,6 @@
#version 330
uniform sampler2D qt_Texture0;
uniform sampler3D lut_table;
uniform sampler2DArray colormap;
uniform vec3 mtf_param[3];
uniform vec2 unit_scale;
uniform bool bw;
@@ -8,6 +8,7 @@ uniform bool invert;
uniform bool srgb;
uniform bool false_color;
uniform int filtering;
uniform int colormapIdx;
in vec2 qt_TexCoord0;
layout(location = 0) out vec4 color;
@@ -22,22 +23,14 @@ vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
{
x = (x - low) / (high - low);
x = clamp(x, vec4(0.0), vec4(1.0));
return ((mid - 1) * x) / ((2 * mid - 1) * x - mid);
return ((mid - 1.0) * x) / ((2.0 * mid - 1.0) * x - mid);
}
vec3 falsecolor(float color)
{
const vec3 pallete[] = vec3[](
vec3(1.0, 0.0, 1.0), //magneta
vec3(0.0, 0.0, 1.0), //blue
vec3(0.0, 1.0, 1.0), //cyan
vec3(0.0, 1.0, 0.0), //green
vec3(1.0, 1.0, 0.0), //yellow
vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0));//red
color *= 5.0;
int i = int(color);
float f = fract(color);
return mix(pallete[i], pallete[i+1], f);// * (f * 0.5 + 0.5);
color *= 255.0 / 256.0;
color += 0.5 / 256.0;
return texture(colormap, vec3(color, 0.5, colormapIdx)).rgb;
}
vec3 checker()
@@ -59,7 +52,7 @@ vec4 cubic(float v)
vec4 textureBicubic(sampler2D sampler, vec2 texCoords)
{
vec2 texSize = textureSize(sampler, 0);
vec2 texSize = vec2(textureSize(sampler, 0));
vec2 invTexSize = 1.0 / texSize;
texCoords = texCoords * texSize - 0.5;
@@ -133,7 +126,7 @@ void main(void)
switch(filtering)
{
case 0://nearest
color = texelFetch(qt_Texture0, ivec2(qt_TexCoord0 * textureSize(qt_Texture0, 0)), 0);
color = texelFetch(qt_Texture0, ivec2(qt_TexCoord0 * vec2(textureSize(qt_Texture0, 0))), 0);
break;
default:
case 1://bilinear
@@ -153,7 +146,14 @@ void main(void)
color.rgb = mix(checker(), color.rgb, color.a);
if(srgb)color.rgb = Linear2sRGB(color.rgb);
if(srgb)
{
color.rgb *= 31.0 / 32.0;
color.rgb += 0.5 / 32.0;
vec4 lut = texture(lut_table, vec3(color.rgb));
color.rgb = lut.rgb;
//color.rgb = Linear2sRGB(lut.rgb);
}
if(any(lessThan(qt_TexCoord0, vec2(0.0))) || any(greaterThan(qt_TexCoord0, vec2(1.0))))
color = vec4(0.0, 0.0, 0.0, 1.0);
-2
View File
@@ -1,5 +1,3 @@
#version 330
uniform sampler2D qt_Texture0;
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
+1 -3
View File
@@ -1,5 +1,3 @@
#version 330
uniform sampler2DArray qt_Texture0;
uniform vec3 mtf_param[3];
uniform bool invert;
@@ -10,7 +8,7 @@ vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
{
x = (x - low) / (high - low);
x = clamp(x, vec4(0.0), vec4(1.0));
return ((mid - 1) * x) / ((2 * mid - 1) * x - mid);
return ((mid - 1.0) * x) / ((2.0 * mid - 1.0) * x - mid);
}
void main(void)
+3 -5
View File
@@ -1,5 +1,3 @@
#version 330
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
in ivec3 imageSize_num;
@@ -13,9 +11,9 @@ void main(void)
{
vec2 pos = qt_Vertex * 0.5;
pos.y *= -1.0;
pos = pos * imageSize_num.xy + thumb_size.x;
pos = pos * vec2(imageSize_num.xy) + float(thumb_size.x);
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * thumb_size.yz;
gl_Position = mvp * vec4(pos - offset + off, 0.0, 1.0);
qt_TexCoord0 = vec3(qt_MultiTexCoord0, imageSize_num.z + 0.1);
gl_Position = mvp * vec4(pos - offset + vec2(off), 0.0, 1.0);
qt_TexCoord0 = vec3(qt_MultiTexCoord0, float(imageSize_num.z) + 0.1);
}
+105 -1
View File
@@ -32,6 +32,8 @@
<li>Convert CFA images to colour - debayer</li>
<li>Color space aware</li>
<li>Histogram</li>
<li>Scripting</li>
<li>Plate solving</li>
</ul>
</description>
<categories>
@@ -45,6 +47,7 @@
</keywords>
<url type="homepage">https://nouspiro.space/?page_id=206</url>
<url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url>
<url type="vcs-browser">https://gitea.nouspiro.space/nou/tenmon</url>
<screenshots>
<screenshot type="default">
<caption>Main window with image</caption>
@@ -57,7 +60,108 @@
</screenshots>
<content_rating type="oars-1.1"/>
<releases>
<release version="20240610" date="2024-06-10">
<release version="20260412" date="2026-04-12">
<description>
<ul>
<li>Add database tree to show database entries grouped in tree structure</li>
<li>Add database summary table. This can show total exposure time for OBJECT and FILTER for example</li>
<li>Copy files to clipboard from database view</li>
<li>Copy to clipboard text from database tables so they can be pasted to Excel</li>
<li>Add CLI option to run scripts and generate thumbnails</li>
</ul>
</description>
</release>
<release version="20260217" date="2026-02-17">
<description>
<ul>
<li>Fix potentional crash</li>
<li>Enable sorting of FITS info</li>
</ul>
</description>
</release>
<release version="20251101" date="2025-11-01">
<description>
<ul>
<li>Better image Save as</li>
<li>Fix xisf file corruption when platesolving</li>
<li>Add selecting language</li>
</ul>
</description>
</release>
<release version="20250915" date="2025-09-15">
<description>
<ul>
<li>Draw equatorial grid and objects overlay</li>
<li>File Manager</li>
<li>Support for PCL:AstrometricSolution</li>
<li>Script console</li>
</ul>
</description>
</release>
<release version="20250429" date="2025-04-29">
<description>
<ul>
<li>Add ability to load multiple images in single file</li>
<li>New plot() and question() script methods</li>
<li>Color highlight of FITS keywords</li>
<li>New scripts to batch platesolve and measure stars</li>
<li>Stretch toolbar can now be vertical</li>
</ul>
</description>
</release>
<release version="20250318" date="2025-03-18">
<description>
<ul>
<li>Fix OpenGL ES drawings</li>
<li>Fix mark/unmark files from script</li>
<li>Fix stretching of float images with values outside of 0-1 range</li>
</ul>
</description>
</release>
<release version="20250302" date="2025-03-02">
<description>
<ul>
<li>Add resize and binning to script</li>
<li>Auto stretch to script</li>
<li>Fix opening UNC paths starting</li>
<li>Add more color maps for false color</li>
<li>Open image with best fit</li>
</ul>
</description>
</release>
<release version="20250126" date="2025-01-26">
<description>
<ul>
<li>Support for really big images +50000px</li>
<li>Fix handling of MAX_PATH on Windows</li>
<li>Add setting solver profile in scripts</li>
</ul>
</description>
</release>
<release version="20241116" date="2024-11-16">
<description>
<ul>
<li>Extending support of data formats</li>
</ul>
</description>
</release>
<release version="20241002" date="2024-10-02">
<description>
<ul>
<li>Plate solving</li>
<li>Open marked files as directory</li>
<li>Linux ARM port</li>
<li>Improved ICC color profile handling</li>
<li>Add *.fz and *.fts as FITS extension</li>
</ul>
</description>
</release>
<release version="20240816" date="2024-08-16">
<description>
<p>Fix saving image</p>
</description>
</release>
<release version="20240616" date="2024-06-16">
<description>
<ul>
<li>Batch processing with JavaScript</li>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="image/x-xisf">
<comment>Extensible Image Serialization Format</comment>
<glob pattern="*.xisf"/>
</mime-type>
</mime-info>
+15 -8
View File
@@ -15,11 +15,13 @@ About::About(QWidget *parent) : QDialog(parent)
QLabel *label = new QLabel(this);
QFile tenmonText(":/about/tenmon");
tenmonText.open(QIODevice::ReadOnly);
if(tenmonText.open(QIODevice::ReadOnly))
{
QByteArray text = tenmonText.readAll();
text.replace("@GITVERSION@", GITVERSION);
label->setText(text);
label->setOpenExternalLinks(true);
}
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
@@ -31,18 +33,23 @@ About::About(QWidget *parent) : QDialog(parent)
HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Help"));
resize(800, 600);
QLocale locale;
QString l = QLocale::languageToString(locale.language());
resize(1000, 600);
setModal(false);
setAttribute(Qt::WA_DeleteOnClose, true);
QVBoxLayout *layout = new QVBoxLayout(this);
QTextEdit *helpText = new QTextEdit(this);
helpText->setReadOnly(true);
layout->addWidget(helpText);
QFile tenmonText(":/help");
tenmonText.open(QIODevice::ReadOnly);
if(tenmonText.open(QIODevice::ReadOnly))
helpText->setHtml(tenmonText.readAll());
layout->addWidget(helpText);
}
QString getVersion()
{
QString version = GITVERSION;
version.truncate(8);
return version;
}
+2
View File
@@ -17,4 +17,6 @@ public:
HelpDialog(QWidget *parent = nullptr);
};
QString getVersion();
#endif // ABOUT_H
+226 -24
View File
@@ -10,10 +10,14 @@
#include <QMessageBox>
#include <QDesktopServices>
#include <QInputDialog>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QCompleter>
#include "scriptengine.h"
#include "chartgraph.h"
#ifdef Q_OS_LINUX
#include <QCloseEvent>
#include <QDBusConnection>
#include <QDBusMessage>
#endif
@@ -57,14 +61,18 @@ void BatchProcessing::scanScriptDir()
_ui->scriptsList->clear();
QDir dir(_scriptBasePath);
QDir embededDir(":/scripts");
QStringList scripts = dir.entryList(QDir::Files | QDir::Readable);
scripts.append(embededDir.entryList(QDir::Files));
scripts.removeDuplicates();
_ui->scriptsList->addItems(scripts);
int idx = scripts.indexOf(current);
if(idx>=0)_ui->scriptsList->setCurrentRow(idx);
}
BatchProcessing::BatchProcessing(QWidget *parent) : QDialog(parent)
BatchProcessing::BatchProcessing(Database *database, QWidget *parent) : QDialog(parent)
, _database(database)
{
_ui = new Ui::BatchProcessing;
_ui->setupUi(this);
@@ -90,17 +98,43 @@ BatchProcessing::BatchProcessing(QWidget *parent) : QDialog(parent)
qWarning() << "Failed to get app data location";
}
connect(_ui->addFilesButton, &QPushButton::released, this, &BatchProcessing::addFiles);
connect(_ui->addDirButton, &QPushButton::released, this, &BatchProcessing::addDir);
connect(_ui->addFilesButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addFiles));
connect(_ui->addDirButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addDir));
connect(_ui->addMarkedButton, &QPushButton::released, this, &BatchProcessing::addMarked);
connect(_ui->removeButton, &QPushButton::released, this, &BatchProcessing::removePath);
connect(_ui->removeAllButton, &QPushButton::released, this, &BatchProcessing::removeAllPaths);
connect(_ui->startButton, &QPushButton::released, this, &BatchProcessing::runScript);
connect(_ui->startButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::runScript));
connect(_ui->stopButton, &QPushButton::released, this, &BatchProcessing::stopScript);
connect(_ui->browseButton, &QPushButton::released, this, &BatchProcessing::browse);
connect(_ui->openScriptsButton, &QPushButton::released, this, &BatchProcessing::openScriptDir);
_textColor = _ui->log->palette().text().color();
_engine = new Script::ScriptEngine(_database, this);
connect(_engine, &Script::ScriptEngine::newMessage, this, &BatchProcessing::newMessage);
_completerModel = new QStringListModel(this);
_completer = new QCompleter(_completerModel, this);
_ui->consoleLineEdit->setCompleter(_completer);
connect(_ui->executeButton, &QPushButton::clicked, _ui->consoleLineEdit, &QLineEdit::returnPressed);
connect(_ui->consoleLineEdit, &QLineEdit::returnPressed, [this](){
if(!_completer->popup()->isVisible())
{
QString program = _ui->consoleLineEdit->text();
QJSValue val = _engine->eval(program);
_ui->consoleLineEdit->addLine();
//qDebug() << val.toString();
}
});
connect(_ui->consoleLineEdit, &QLineEdit::textEdited, [this](const QString &text){
QStringList comp = _engine->complete(text);
//qDebug() << comp;
_completerModel->setStringList(comp);
});
_ui->addFilesButton->setAutoDefault(false);
QSettings settings;
_ui->outputPath->setText(settings.value("batchprocessing/outputpath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString());
}
@@ -113,6 +147,17 @@ BatchProcessing::~BatchProcessing()
delete _ui;
}
void BatchProcessing::setOutputDir(const QString &output)
{
_ui->outputPath->setText(output);
}
void BatchProcessing::setPaths(const QStringList &paths)
{
_ui->pathsList->addItems(paths);
refreshPaths();
}
void BatchProcessing::closeEvent(QCloseEvent *event)
{
if(_engineThread)
@@ -134,6 +179,15 @@ void BatchProcessing::closeEvent(QCloseEvent *event)
}
}
void BatchProcessing::refreshPaths()
{
QStringList paths;
for(int i=0; i<_ui->pathsList->count(); i++)
paths.append(_ui->pathsList->item(i)->text());
_paths = scanDirectories(paths);
_engine->setParams("", _paths, _ui->outputPath->text(), QString());
}
void BatchProcessing::addFiles()
{
QSettings settings;
@@ -143,6 +197,7 @@ void BatchProcessing::addFiles()
_ui->pathsList->addItems(files);
settings.setValue("batchprocessing/inputpath", QFileInfo(files.first()).absolutePath());
}
refreshPaths();
}
void BatchProcessing::addDir()
@@ -154,41 +209,44 @@ void BatchProcessing::addDir()
_ui->pathsList->addItem(dir);
settings.setValue("batchprocessing/inputpath", dir);
}
refreshPaths();
}
void BatchProcessing::addMarked()
{
QStringList files = _database->getMarkedFiles();
for(const QString &file : files)
{
QFileInfo info(file);
if(info.exists() && info.isReadable())
_ui->pathsList->addItem(file);
};
refreshPaths();
}
void BatchProcessing::removePath()
{
for(auto &item : _ui->pathsList->selectedItems())
delete item;
refreshPaths();
}
void BatchProcessing::removeAllPaths()
{
_ui->pathsList->clear();
refreshPaths();
}
void BatchProcessing::browse()
{
QString output = QFileDialog::getExistingDirectory(this, tr("Select output directory"), "/home/nou/Obrázky");
QString output = QFileDialog::getExistingDirectory(this, tr("Select output directory"), _ui->outputPath->text());
if(!output.isEmpty())
_ui->outputPath->setText(output);
}
void BatchProcessing::openScriptDir()
{
#ifdef Q_OS_LINUX
QDBusConnection con = QDBusConnection::sessionBus();
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
QList<QVariant> args = {QStringList(QUrl::fromLocalFile(_scriptBasePath).toString()), QString()};
message.setArguments(args);
con.call(message);
#endif
#ifdef Q_OS_WINDOWS
QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(_scriptBasePath)});
#endif
#ifdef Q_OS_MACOS
QDesktopServices::openUrl(QUrl::fromLocalFile(_scriptBasePath));
#endif
openDir(_scriptBasePath);
}
void BatchProcessing::runScript()
@@ -197,17 +255,20 @@ void BatchProcessing::runScript()
auto selectedItems = _ui->scriptsList->selectedItems();
if(selectedItems.size())
{
_engineThread = new Script::ScriptEngineThread(this);
_engineThread = new Script::ScriptEngineThread(_database, this);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
QStringList paths;
for(int i=0; i<_ui->pathsList->count(); i++)
paths.append(_ui->pathsList->item(i)->text());
QFileInfo outDir(_ui->outputPath->text());
if(outDir.exists() && outDir.isWritable())
{
_engineThread->setParams(_scriptBasePath + selectedItems.first()->text(), scanDirectories(paths), _ui->outputPath->text());
QString script = selectedItems.first()->text();
if(QDir(_scriptBasePath).exists(script))
script = _scriptBasePath + script;
else
script = ":/scripts/" + script;
_engineThread->setParams(script, _paths, _ui->outputPath->text(), QString());
_engineThread->start();
_ui->startButton->setEnabled(false);
_ui->stopButton->setEnabled(true);
@@ -215,6 +276,35 @@ void BatchProcessing::runScript()
else
{
QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
delete _engineThread;
_engineThread = nullptr;
}
}
}
void BatchProcessing::runScript(const QString &script, const QString &arg, bool exit)
{
_ui->log->clear();
{
_engineThread = new Script::ScriptEngineThread(_database, this);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessageCli);
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
if(exit)connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::accept);
QFileInfo outDir(_ui->outputPath->text());
if(outDir.exists() && outDir.isWritable())
{
_engineThread->setParams(script, _paths, _ui->outputPath->text(), arg);
_engineThread->start();
_ui->startButton->setEnabled(false);
_ui->stopButton->setEnabled(true);
}
else
{
QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
delete _engineThread;
_engineThread = nullptr;
}
}
}
@@ -242,6 +332,14 @@ void BatchProcessing::newMessage(const QString &message, bool error)
_ui->log->append(message);
}
void BatchProcessing::newMessageCli(const QString &message, bool error)
{
if(error)
qWarning() << message;
else
qInfo() << message;
}
QJSValue BatchProcessing::getString(const QString &label, const QString &text)
{
bool ok = false;
@@ -269,3 +367,107 @@ QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label
QString ret = QInputDialog::getItem(this, tr("Select item"), label, items, current, false, &ok);
return ok ? ret : QJSValue();
}
QJSValue BatchProcessing::question(const QString &question, const QStringList &buttons, const QString &title)
{
QMessageBox::StandardButtons standardButtons = QMessageBox::NoButton;
if(buttons.contains("ok"))standardButtons |= QMessageBox::Ok;
if(buttons.contains("yes"))standardButtons |= QMessageBox::Yes;
if(buttons.contains("no"))standardButtons |= QMessageBox::No;
if(buttons.contains("yesall"))standardButtons |= QMessageBox::YesToAll;
if(buttons.contains("noall"))standardButtons |= QMessageBox::NoToAll;
if(buttons.contains("abort"))standardButtons |= QMessageBox::Abort;
if(buttons.contains("retry"))standardButtons |= QMessageBox::Retry;
if(buttons.contains("ignore"))standardButtons |= QMessageBox::Ignore;
if(buttons.contains("cancel"))standardButtons |= QMessageBox::Cancel;
if(buttons.contains("discard"))standardButtons |= QMessageBox::Discard;
if(buttons.contains("apply"))standardButtons |= QMessageBox::Apply;
if(buttons.contains("reset"))standardButtons |= QMessageBox::Reset;
if(standardButtons == QMessageBox::NoButton)standardButtons = QMessageBox::Ok;
QMessageBox::StandardButton button = QMessageBox::question(this, title, question, standardButtons);
QJSValue ret;
switch(button)
{
default:
case QMessageBox::Ok: ret = "ok"; break;
case QMessageBox::Yes: ret = "yes"; break;
case QMessageBox::No: ret = "no"; break;
case QMessageBox::YesToAll: ret = "yesall"; break;
case QMessageBox::NoToAll: ret = "noall"; break;
case QMessageBox::Abort: ret = "abort"; break;
case QMessageBox::Retry: ret = "retry"; break;
case QMessageBox::Ignore: ret = "ignore"; break;
case QMessageBox::Cancel: ret = "cancel"; break;
case QMessageBox::Discard: ret = "discard"; break;
case QMessageBox::Apply: ret = "apply"; break;
case QMessageBox::Reset: ret = "reset"; break;
}
return ret;
}
void BatchProcessing::plot(const QVariant &graph)
{
ChartGraph *chart = new ChartGraph(this);
chart->plot(graph);
}
ConsoleLine::ConsoleLine(QWidget *parent) : QLineEdit(parent)
{
}
void ConsoleLine::addLine()
{
QString line = text();
clear();
if(_history.size() && _history.last() == line)return;
_history.append(line);
if(_history.size() > 100)_history.removeFirst();
_currentLine = _history.size();
}
void ConsoleLine::keyReleaseEvent(QKeyEvent *event)
{
if(event->key() == Qt::Key_Up)
{
_currentLine--;
if(_currentLine < 0)
{
_currentLine = -1;
clear();
return;
}
setText(_history.at(_currentLine));
}
else if(event->key() == Qt::Key_Down)
{
_currentLine++;
if(_currentLine >= _history.size())
{
_currentLine = _history.size();
clear();
return;
}
setText(_history.at(_currentLine));
}
else QLineEdit::keyReleaseEvent(event);
}
void openDir(const QString &path)
{
#ifdef Q_OS_LINUX
QDBusConnection con = QDBusConnection::sessionBus();
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
QList<QVariant> args = {QStringList(QUrl::fromLocalFile(path).toString()), QString()};
message.setArguments(args);
con.call(message);
#endif
#ifdef Q_OS_WINDOWS
QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(path)});
#endif
#ifdef Q_OS_MACOS
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
#endif
}
+34 -1
View File
@@ -3,10 +3,15 @@
#include <QDialog>
#include <QFileSystemWatcher>
#include <QStringListModel>
#include <QCompleter>
#include <QLineEdit>
#include "scriptengine.h"
namespace Ui { class BatchProcessing; }
class Database;
class BatchProcessing : public QDialog
{
Q_OBJECT
@@ -14,30 +19,58 @@ class BatchProcessing : public QDialog
QString _scriptBasePath;
QFileSystemWatcher _fileWatcher;
Script::ScriptEngineThread *_engineThread = nullptr;
Script::ScriptEngine *_engine = nullptr;
QColor _textColor;
Database *_database;
QStringListModel *_completerModel = nullptr;
QCompleter *_completer = nullptr;
QList<QPair<QString, QString>> _paths;
private slots:
void scanScriptDir();
public:
explicit BatchProcessing(QWidget *parent = nullptr);
explicit BatchProcessing(Database *database, QWidget *parent = nullptr);
~BatchProcessing();
void setOutputDir(const QString &output);
void setPaths(const QStringList &paths);
protected:
void closeEvent(QCloseEvent *event);
void refreshPaths();
public slots:
void addFiles();
void addDir();
void addMarked();
void removePath();
void removeAllPaths();
void browse();
void openScriptDir();
void runScript();
void runScript(const QString &script, const QString &arg, bool exit);
void stopScript();
void scriptFinished();
void newMessage(const QString &message, bool error);
void newMessageCli(const QString &message, bool error);
QJSValue getString(const QString &label, const QString &text);
QJSValue getInt(const QString &label, int value);
QJSValue getFloat(const QString &label, double value, int decimals);
QJSValue getItem(const QStringList &items, const QString &label, int current);
QJSValue question(const QString &question, const QStringList &buttons, const QString &title = "");
void plot(const QVariant &graph);
};
class ConsoleLine : public QLineEdit
{
Q_OBJECT
public:
explicit ConsoleLine(QWidget *parent = nullptr);
void addLine();
void keyReleaseEvent(QKeyEvent *event) override;
private:
int _currentLine = 0;
QStringList _history;
};
void openDir(const QString &path);
#endif // BATCHPROCESSING_H
+70 -33
View File
@@ -43,6 +43,9 @@
<property name="text">
<string>Add files</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -50,6 +53,19 @@
<property name="text">
<string>Add directories</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addMarkedButton">
<property name="text">
<string>Add marked</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -57,6 +73,9 @@
<property name="text">
<string>Remove</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -64,6 +83,9 @@
<property name="text">
<string>Remove all</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -91,6 +113,9 @@
<property name="text">
<string>Browse</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -122,6 +147,9 @@
<property name="text">
<string>Open scripts</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -164,23 +192,30 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<widget class="ConsoleLine" name="consoleLineEdit">
<property name="placeholderText">
<string>Console</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</widget>
</item>
<item>
<widget class="QPushButton" name="executeButton">
<property name="text">
<string>Execute</string>
</property>
</spacer>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="startButton">
<property name="text">
<string>Start script</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -191,12 +226,8 @@
<property name="text">
<string>Stop script</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
@@ -204,23 +235,29 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ConsoleLine</class>
<extends>QLineEdit</extends>
<header>batchprocessing.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>pathsList</tabstop>
<tabstop>addFilesButton</tabstop>
<tabstop>addDirButton</tabstop>
<tabstop>addMarkedButton</tabstop>
<tabstop>removeButton</tabstop>
<tabstop>removeAllButton</tabstop>
<tabstop>browseButton</tabstop>
<tabstop>openScriptsButton</tabstop>
<tabstop>scriptsList</tabstop>
<tabstop>consoleLineEdit</tabstop>
<tabstop>startButton</tabstop>
<tabstop>stopButton</tabstop>
<tabstop>outputPath</tabstop>
<tabstop>log</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>closeButton</sender>
<signal>released()</signal>
<receiver>BatchProcessing</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>973</x>
<y>745</y>
</hint>
<hint type="destinationlabel">
<x>511</x>
<y>383</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>
+298
View File
@@ -0,0 +1,298 @@
#include "chartgraph.h"
#include <QChartView>
#include <QVBoxLayout>
#include <QLineSeries>
#include <QBarSeries>
#include <QBarSet>
#include <QBarCategoryAxis>
#include <QScatterSeries>
#include <QMenu>
#include <QMenuBar>
#include <QValueAxis>
#include <QFileDialog>
#include <QSettings>
#include <QToolBar>
#include <QStyle>
class ChartView : public QChartView
{
QPointF _mousePos;
bool _scroll = false;
public:
ChartView(QWidget *parent) : QChartView(parent)
{
}
protected:
void keyPressEvent(QKeyEvent *event) override
{
if(!chart()->isZoomed())chart()->zoom(0.999999);//workaround so zoomReset() reset scroll
switch(event->key())
{
case Qt::Key_Plus:
chart()->zoomIn();
break;
case Qt::Key_Minus:
chart()->zoomOut();
break;
case Qt::Key_Left:
chart()->scroll(-10, 0);
break;
case Qt::Key_Right:
chart()->scroll(10, 0);
break;
case Qt::Key_Up:
chart()->scroll(0, 10);
break;
case Qt::Key_Down:
chart()->scroll(0, -10);
break;
default:
QGraphicsView::keyPressEvent(event);
break;
}
}
void mousePressEvent(QMouseEvent *event) override
{
if(event->button() == Qt::LeftButton)
{
_scroll = true;
if(!chart()->isZoomed())chart()->zoom(0.999999);//workaround so zoomReset() reset scroll
_mousePos = event->position();
}
QChartView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event) override
{
if(_scroll)
{
QPointF pos = event->position();
chart()->scroll(_mousePos.x() - pos.x(), pos.y() - _mousePos.y());
_mousePos = pos;
}
QChartView::mouseMoveEvent(event);
}
void mouseReleaseEvent(QMouseEvent *event) override
{
_scroll = false;
QChartView::mouseReleaseEvent(event);
}
void wheelEvent(QWheelEvent *event) override
{
if(event->angleDelta().y() > 0)
chart()->zoomIn();
if(event->angleDelta().y() < 0)
chart()->zoomOut();
}
};
ChartGraph::ChartGraph(QWidget *parent) : QMainWindow(parent)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Chart"));
_chartView = new ChartView(this);
setCentralWidget(_chartView);
_chart = new QChart;
_chartView->setChart(_chart);
_chartView->setRenderHint(QPainter::Antialiasing);
resize(1024, 768);
menuBar()->addAction(tr("Save"), this, &ChartGraph::save);
menuBar()->addAction(tr("Reset view"), [this](){ _chart->zoomReset(); });
}
void ChartGraph::plot(const QVariant &graph)
{
QVariantMap map = graph.toMap();
_chart->setTitle(map["title"].toString());
if(map.contains("legend"))
{
QVariantMap legend = map["legend"].toMap();
if(legend.contains("visible"))
_chart->legend()->setVisible(legend["visible"].toBool());
QString align = legend["align"].toString();
if(align == "top")
_chart->legend()->setAlignment(Qt::AlignTop);
else if(align == "left")
_chart->legend()->setAlignment(Qt::AlignLeft);
else if(align == "bottom")
_chart->legend()->setAlignment(Qt::AlignBottom);
else if(align == "right")
_chart->legend()->setAlignment(Qt::AlignRight);
}
QBarSeries *barSeries = nullptr;
qreal minX = INFINITY;
qreal maxX = -INFINITY;
qreal minY = INFINITY;
qreal maxY = -INFINITY;
qreal minY2 = INFINITY;
qreal maxY2 = -INFINITY;
QValueAxis *xaxis = new QValueAxis(_chart);
QBarCategoryAxis *barxaxis = new QBarCategoryAxis(_chart);
QValueAxis *yaxis = new QValueAxis(_chart);
QValueAxis *y2axis = new QValueAxis(_chart);
_chart->addAxis(xaxis, Qt::AlignBottom);
_chart->addAxis(yaxis, Qt::AlignLeft);
_chart->addAxis(y2axis, Qt::AlignRight);
_chart->addAxis(barxaxis, Qt::AlignBottom);
y2axis->setGridLinePen(Qt::DashDotLine);
for(auto s : map["series"].toList())
{
QVariantMap serie = s.toMap();
QString type = serie["type"].toString();
bool y2 = serie["y2"].toBool();
if(type == "line" || type == "points" || type == "linePoints" || type.isEmpty())
{
QXYSeries *series = nullptr;
if(type == "points")
{
QScatterSeries *scatter = new QScatterSeries(_chart);
series = scatter;
QString shape = serie["shape"].toString();
if(shape == "circle")scatter->setMarkerShape(QScatterSeries::MarkerShapeCircle);
else if(shape == "rectangle")scatter->setMarkerShape(QScatterSeries::MarkerShapeRectangle);
else if(shape == "triangle")scatter->setMarkerShape(QScatterSeries::MarkerShapeTriangle);
else if(shape == "star")scatter->setMarkerShape(QScatterSeries::MarkerShapeStar);
else if(shape == "pentagon")scatter->setMarkerShape(QScatterSeries::MarkerShapePentagon);
}
else
{
series = new QLineSeries(_chart);
}
series->setName(serie["title"].toString());
QVariantList x = serie["x"].toList();
QVariantList y = serie["y"].toList();
if(x.isEmpty())
{
for(int i = 0; i < y.size(); i++)
{
qreal val = y[i].toDouble();
if(y2)
{
minY2 = std::min(minY2, val);
maxY2 = std::max(maxY2, val);
}
else
{
minY = std::min(minY, val);
maxY = std::max(maxY, val);
}
series->append(i, val);
}
minX = std::min(minX, 0.0);
maxX = std::max(maxX, y.size() - 1.0);
}
else
{
int size = std::min(x.size(), y.size());
for(int i = 0; i < size; i++)
{
qreal val = y[i].toDouble();
if(y2)
{
minY2 = std::min(minY2, val);
maxY2 = std::max(maxY2, val);
}
else
{
minY = std::min(minY, val);
maxY = std::max(maxY, val);
}
minX = std::min(minX, x[i].toDouble());
maxX = std::max(maxX, x[i].toDouble());
series->append(x[i].toDouble(), val);
}
}
_chart->addSeries(series);
series->attachAxis(xaxis);
series->attachAxis(y2 ? y2axis : yaxis);
if(serie.contains("color"))
{
QString color = serie["color"].toString();
if(QColor::isValidColorName(color))series->setColor(QColor::fromString(color));
}
if(serie["bestFit"].toBool())
{
series->setBestFitLineVisible(true);
QPen pen = series->bestFitLinePen();
pen.setColor(series->color());
pen.setStyle(Qt::DashLine);
series->setBestFitLinePen(pen);
}
if(type == "linePoints")
series->setPointsVisible(true);
}
else if(type == "bar")
{
if(!barSeries)
{
barSeries = new QBarSeries(_chart);
_chart->addSeries(barSeries);
barSeries->attachAxis(yaxis);
barSeries->attachAxis(barxaxis);
}
QBarSet *set = new QBarSet(serie["title"].toString());
QVariantList y = serie["y"].toList();
for(int i = 0; i < y.size(); i++)
{
qreal val = y[i].toDouble();
minY = std::min(minY, val);
maxY = std::max(maxY, val);
set->append(val);
}
barSeries->append(set);
for(int i = barxaxis->count() + 1; i <= y.size(); i++)
barxaxis->append(QString::number(i));
if(serie.contains("color"))
{
QString color = serie["color"].toString();
if(QColor::isValidColorName(color))set->setColor(QColor::fromString(color));
}
}
}
if(barSeries)
{
xaxis->setRange(std::min(minX, -0.5), std::max(maxX, barxaxis->count() - 0.5));
minY = std::min(minY, 0.0);
}
else
{
xaxis->setRange(minX, maxX);
}
yaxis->setRange(minY, maxY);
y2axis->setRange(minY2, maxY2);
show();
}
void ChartGraph::save()
{
QSettings settings;
QString dir = settings.value("mainwindow/lastdir").toString();
QString output = QFileDialog::getSaveFileName(this, tr("Save as"), dir, "PNG (*.png)");
if(!output.isEmpty())
{
QPixmap graph = _chartView->grab();
graph.toImage().save(output);
}
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef CHARTGRAPH_H
#define CHARTGRAPH_H
#include <QMainWindow>
#include <QJSValue>
#include <QChart>
class ChartView;
class ChartGraph : public QMainWindow
{
Q_OBJECT
QChart *_chart;
ChartView *_chartView;
public:
explicit ChartGraph(QWidget *parent = nullptr);
void plot(const QVariant &graph);
public slots:
void save();
};
#endif // CHARTGRAPH_H
+135 -46
View File
@@ -4,7 +4,7 @@
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
#include "loadrunable.h"
#include "loadimage.h"
Database::Database(QObject *parent) : QObject(parent)
{
@@ -15,66 +15,101 @@ bool Database::init(const QLatin1String &connectionName)
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(path);
QSqlDatabase m_database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
ngc = QSqlDatabase::addDatabase("QSQLITE", connectionName + "ngc");
if(!dir.mkpath("."))
return false;
if(m_database.isValid())
if(ngc.isValid())
{
m_database.setDatabaseName(dir.absoluteFilePath("database2.db"));
if(m_database.open())
QString ngcDb = dir.absoluteFilePath("ngc.db");
if(!QFile::exists(ngcDb))
QFile::copy(":/ngc.db", ngcDb);
ngc.setDatabaseName(ngcDb);
if(ngc.open())
{
m_database.exec("PRAGMA foreign_keys = ON");
int version = checkVersion();
m_getNgc = QSqlQuery(ngc);
m_getNgc.prepare("SELECT *,IIF(V_Mag IS NULL, B_Mag, V_Mag) AS mag FROM ngc WHERE RA_deg BETWEEN ? AND ? AND DEC_deg BETWEEN ? AND ?");
}
else
{
qWarning() << "Could not open NGC database";
}
}
if(database.isValid())
{
database.setDatabaseName(dir.absoluteFilePath("database2.db"));
if(database.open())
{
QSqlQuery query(database);
query.exec("PRAGMA foreign_keys = ON");
int version = checkVersion(database);
if(version == 0)
{
m_database.exec("PRAGMA user_version = 1");
m_database.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
m_database.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
query.exec("PRAGMA user_version = 1");
query.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
query.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
" minRa REAL, maxRa REAL, minDec REAL, maxDec REAL, crVal1 REAL, crVal2 REAL)");
m_database.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
query.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
"key VARCHAR(81), value VARCHAR(81), comment VARCHAR(81), FOREIGN KEY(id_file) REFERENCES fits_files(id) ON DELETE CASCADE)");
m_database.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
m_database.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
m_database.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
m_database.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
m_database.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
m_database.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
query.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
query.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
query.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
query.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
query.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
version = 1;
}
else if(version > 1)
if(version == 1)
{
qDebug() << "Database version is too new";
query.exec("CREATE INDEX IF NOT EXISTS id_file_key ON fits_headers(id_file, key)");
query.exec("PRAGMA user_version = 2");
version = 2;
}
if(version > 2)
{
qWarning() << "Database version is too new";
return false;
}
QSqlError error = m_database.lastError();
QSqlError error = database.lastError();
if(error.type() == QSqlError::NoError)
{
m_markQuery = QSqlQuery(m_database);
m_markQuery = QSqlQuery(database);
m_markQuery.prepare("INSERT INTO files (file) VALUES (?)");
m_unmarkQuery = QSqlQuery(m_database);
m_unmarkQuery = QSqlQuery(database);
m_unmarkQuery.prepare("DELETE FROM files WHERE file = (?)");
m_isMarkedQuery = QSqlQuery(m_database);
m_isMarkedQuery = QSqlQuery(database);
m_isMarkedQuery.prepare("SELECT * FROM files WHERE file = (:name)");
m_insertFile = QSqlQuery(m_database);
m_insertFile = QSqlQuery(database);
m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
m_insertFileWcs = QSqlQuery(m_database);
m_insertFileWcs = QSqlQuery(database);
m_insertFileWcs.prepare("INSERT INTO fits_files (file, mtime, minRa, maxRa, minDec, maxDec, crVal1, crVal2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
m_insertFitsHeader = QSqlQuery(m_database);
m_insertFitsHeader = QSqlQuery(database);
m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
m_checkFile = QSqlQuery(m_database);
m_checkFile = QSqlQuery(database);
m_checkFile.prepare("SELECT id,mtime FROM fits_files WHERE file=?");
m_headerKeywords = QSqlQuery(m_database);
m_headerKeywords = QSqlQuery(database);
m_headerKeywords.prepare("SELECT DISTINCT key FROM fits_headers ORDER BY key");
m_deleteFile = QSqlQuery(m_database);
m_deleteFile = QSqlQuery(database);
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
return true;
}
qDebug() << error.text();
qWarning() << error.text();
}
else
{
qWarning() << "Failed to open database" << connectionName;
}
}
else
{
qWarning() << "Database is invalid";
}
return false;
}
@@ -130,7 +165,7 @@ QStringList Database::getMarkedFiles()
void Database::clearMarkedFiles()
{
QSqlDatabase::database().exec("DELETE FROM files");
QSqlQuery query("DELETE FROM files");
}
bool Database::checkError(QSqlQuery &query)
@@ -140,21 +175,20 @@ bool Database::checkError(QSqlQuery &query)
return true;
else
{
qDebug() << error.text();
qWarning() << error.text();
return false;
}
}
int Database::checkVersion()
int Database::checkVersion(QSqlDatabase &db)
{
QSqlDatabase db = QSqlDatabase::database();
QSqlQuery query = db.exec("PRAGMA user_version");
QSqlQuery query("PRAGMA user_version", db);
if(query.next())
return query.value(0).toInt();
return -1;
}
static QStringList nameFilters = {"*.fit", "*.fits", "*.xisf"};
static QStringList nameFilters = {"*.fit", "*.fits", "*.fz", "*.fts", "*.xisf"};
static int countFiles(const QDir &dir, QStringList &scannedDirs)
{
@@ -174,7 +208,6 @@ void Database::indexDir(const QDir &dir, QProgressDialog *progress)
QStringList scannedDirs;
int count = countFiles(dir, scannedDirs);
progress->setMaximum(count);
QSqlDatabase database = QSqlDatabase::database();
database.transaction();
scannedDirs.clear();
@@ -192,12 +225,11 @@ void Database::indexDir(const QDir &dir, QProgressDialog *progress)
void Database::reindex(QProgressDialog *progress)
{
QVariantList deleteids;
QSqlDatabase database = QSqlDatabase::database();
database.transaction();
QSqlQuery size = database.exec("SELECT COUNT(*) FROM fits_files");
QSqlQuery size("SELECT COUNT(*) FROM fits_files", database);
size.next();
progress->setMaximum(size.value(0).toInt());
QSqlQuery files = database.exec("SELECT id,file,mtime FROM fits_files");
QSqlQuery files("SELECT id,file,mtime FROM fits_files", database);
int i = 0;
while(files.next())
{
@@ -231,6 +263,62 @@ QStringList Database::getFitsKeywords()
return keywords;
}
QVector<SkyObject> Database::getObjects(double minRa, double maxRa, double minDec, double maxDec)
{
QVector<SkyObject> objects;
if(!ngc.isOpen())return objects;
m_getNgc.bindValue(0, minRa);
m_getNgc.bindValue(1, maxRa);
m_getNgc.bindValue(2, minDec);
m_getNgc.bindValue(3, maxDec);
if(m_getNgc.exec())
{
while(m_getNgc.next())
{
QString name;
QString name2;
QString m = m_getNgc.value("M").toString();
QString ic = m_getNgc.value("IC").toString();
if(!m.isEmpty())
{
name = "M" + m;
m.clear();
}
else if(!ic.isEmpty())
{
name = "IC" + ic;
ic.clear();
}
else
{
name = m_getNgc.value("Name").toString();
}
if(!ic.isEmpty())name2 += "IC" + ic + " ";
name2 += m_getNgc.value("Common names").toString();
objects.append({
name,
name2,
{m_getNgc.value("RA_deg").toDouble(), m_getNgc.value("DEC_deg").toDouble()},
m_getNgc.value("MajAx").toDouble(),
m_getNgc.value("MinAx").toDouble(),
m_getNgc.value("PosAng").toDouble(),
m_getNgc.value("mag").isNull() ? NAN : m_getNgc.value("mag").toDouble(),
{0, 0},
});
}
}
return objects;
}
const QSqlDatabase &Database::db() const
{
return database;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
{
if(scannedDirs.contains(dir.canonicalPath()))return true;
@@ -271,10 +359,10 @@ bool Database::indexFile(const QFileInfo &file)
}
}
bool ok;
if(filePath.endsWith(".xisf", Qt::CaseInsensitive))
bool ok = false;
if(isXISF(file.suffix()))
ok = readXISFHeader(filePath, info);
else
else if(isFITS(file.suffix()))
ok = readFITSHeader(filePath, info);
qlonglong last_id = -1;
@@ -295,7 +383,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFileWcs.bindValue(7, crVal2);
if(!m_insertFileWcs.exec())
{
qDebug() << "Database error" << m_insertFileWcs.lastError();
qWarning() << "Database error" << m_insertFileWcs.lastError();
return false;
}
last_id = m_insertFileWcs.lastInsertId().toLongLong();
@@ -306,7 +394,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFile.bindValue(1, mtime);
if(!m_insertFile.exec())
{
qDebug() << "Database error" << m_insertFile.lastError();
qWarning() << "Database error" << m_insertFile.lastError();
return false;
}
last_id = m_insertFile.lastInsertId().toLongLong();
@@ -315,6 +403,7 @@ bool Database::indexFile(const QFileInfo &file)
QVariantList file_id, keys, values, comments;
for(const auto &record : info.fitsHeader)
{
if(record.xisf && record.key.startsWith("PCL:"))continue;
file_id << last_id;
keys << QString(record.key);
values << record.value.toString();
@@ -326,7 +415,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFitsHeader.bindValue(3, comments);
if(!m_insertFitsHeader.execBatch())
{
qDebug() << "Database error" << m_insertFitsHeader.lastError();
qWarning() << "Database error" << m_insertFitsHeader.lastError();
return false;
}
}
+8 -1
View File
@@ -6,10 +6,13 @@
#include <QSqlQuery>
#include <QDir>
#include <QProgressDialog>
#include "imageinfodata.h"
class Database : public QObject
{
Q_OBJECT
QSqlDatabase database;
QSqlDatabase ngc;
QSqlQuery m_markQuery;
QSqlQuery m_unmarkQuery;
QSqlQuery m_isMarkedQuery;
@@ -21,6 +24,8 @@ class Database : public QObject
QSqlQuery m_headerKeywords;
QSqlQuery m_deleteFile;
QSqlQuery m_getNgc;
int m_progress;
public:
explicit Database(QObject *parent = 0);
@@ -36,11 +41,13 @@ public:
void indexDir(const QDir &dir, QProgressDialog *progress);
void reindex(QProgressDialog *progress);
QStringList getFitsKeywords();
QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec);
const QSqlDatabase& db() const;
protected:
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
bool indexFile(const QFileInfo &file);
bool checkError(QSqlQuery &query);
int checkVersion();
int checkVersion(QSqlDatabase &db);
signals:
void databaseChanged();
};
+591
View File
@@ -0,0 +1,591 @@
#include "databasetree.h"
#include "database.h"
#include "databaseview.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QSettings>
#include <QSqlError>
#include <QStackedWidget>
#include <QVBoxLayout>
DatabaseTreeSettings::DatabaseTreeSettings(const QStringList &data, QStringList keywords, QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Add tree grouping"));
QVBoxLayout *vlayout = new QVBoxLayout(this);
setLayout(vlayout);
QStringList key = data[0].split('/');
qsizetype dateobsindex = keywords.indexOf("DATE-OBS");
if(dateobsindex != -1)
{
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH-DAY");
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH");
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR");
}
for(int i = 0; i < 10; i++)
{
QComboBox *comboxBox = new QComboBox(this);
comboxBox->addItem("");
comboxBox->addItems(keywords);
vlayout->addWidget(comboxBox);
_keywordsSelect.append(comboxBox);
if(i < key.size() && keywords.contains(key[i]))
comboxBox->setCurrentText(key[i]);
}
vlayout->addWidget(new QLabel(tr("Aggregate function"), this));
_aggregateFunction = new QComboBox(this);
_aggregateFunction->addItems({"", "SUM", "COUNT", "AVG", "MIN", "MAX", "MEDIAN"});
vlayout->addWidget(_aggregateFunction);
_aggregateFunction->setToolTip(tr("This aggregate function will be applied to last level of grouping"));
_aggregateFunction->setCurrentText(data[1]);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DatabaseTreeSettings::acceptButton);
connect(buttonBox, &QDialogButtonBox::rejected, this, &DatabaseTreeSettings::reject);
vlayout->addWidget(buttonBox);
}
QString DatabaseTreeSettings::keywords() const
{
QStringList keywords;
for(QComboBox *box : _keywordsSelect)
{
if(box->currentIndex() > 0)
keywords.append(box->currentText());
}
return keywords.join('/');
}
QString DatabaseTreeSettings::aggregrationFunc() const
{
return _aggregateFunction->currentText();
}
void DatabaseTreeSettings::acceptButton()
{
for(QComboBox *box : _keywordsSelect)
{
if(box->currentIndex() > 0)
{
QDialog::accept();
return;
}
}
QDialog::reject();
}
class TreeNode
{
public:
TreeNode() = default;
TreeNode(TreeNode *parent, const QVariant value, int level)
:_parent(parent)
,_value(value)
,_level(level)
{}
const TreeNode* child(size_t idx) const
{
if(idx >= 0 && idx < _children.size())
return _children[idx].get();
return nullptr;
}
TreeNode* child(size_t idx)
{
if(idx >= 0 && idx < _children.size())
return _children[idx].get();
return nullptr;
}
TreeNode* parent() const
{
return _parent;
}
int row() const
{
if(_parent)
return _parent->indexOf(this);
return 0;
}
int childCount() const
{
if(!_init)return 1;
return _children.size();
}
const QVariant& value() const
{
return _value;
}
void fill(const QVariantList &list)
{
_init = true;
for(auto &item : list)
_children.push_back(std::make_unique<TreeNode>(this, item, _level + 1));
}
bool filled() const
{
return _init;
}
int level() const
{
return _level;
}
private:
int indexOf(const TreeNode *child) const
{
auto f = [child](const std::unique_ptr<TreeNode> &i){ return i.get() == child; };
auto it = std::find_if(_children.begin(), _children.end(), f);
if(it != _children.end())return std::distance(_children.begin(), it);
return -1;
}
TreeNode *_parent = nullptr;
QVariant _value;
std::vector<std::unique_ptr<TreeNode>> _children;
bool _init = false;
int _level = 0;
};
DatabaseTree::DatabaseTree(Database *database, QObject *parent) : QAbstractItemModel(parent)
,_database(database)
{
_italicFont.setItalic(true);
}
void DatabaseTree::setKeys(const QStringList &keys)
{
_keys = keys;
if(!_loaded)return;
beginResetModel();
prepareQueries();
_rootNode = std::make_unique<TreeNode>();
fillNode(_rootNode.get());
endResetModel();
}
QStringList DatabaseTree::keys() const
{
return _keys;
}
QModelIndex DatabaseTree::index(int row, int column, const QModelIndex &parent) const
{
if(!hasIndex(row, column, parent))
return QModelIndex();
TreeNode *node;
if(!parent.isValid())
node = _rootNode.get();
else
node = static_cast<TreeNode*>(parent.internalPointer());
if(node)
{
TreeNode *child = node->child(row);
if(child)return createIndex(row, column, child);
}
return QModelIndex();
}
QModelIndex DatabaseTree::parent(const QModelIndex &index) const
{
if(!index.isValid())
return QModelIndex();
TreeNode *childNode = static_cast<TreeNode*>(index.internalPointer());
const TreeNode *parentNode = childNode->parent();
if (parentNode == _rootNode.get())
return QModelIndex();
return createIndex(parentNode->row(), 0, parentNode);
}
int DatabaseTree::rowCount(const QModelIndex &index) const
{
if(index.column() > 0)return 0;
TreeNode *node;
if(!index.isValid())
node = _rootNode.get();
else
node = static_cast<TreeNode*>(index.internalPointer());
if(node && node->level() <= _keys.size())
return node->childCount();
return 0;
}
int DatabaseTree::columnCount(const QModelIndex &index) const
{
Q_UNUSED(index);
return 1;
}
QVariant DatabaseTree::data(const QModelIndex &index, int role) const
{
if(!index.isValid())
return QVariant();
TreeNode *node = static_cast<TreeNode*>(index.internalPointer());
if(node == nullptr)
return QVariant();
switch(role)
{
case Qt::FontRole:
{
if(node->value().toString().isNull())
return _italicFont;
return QVariant();
}
case Qt::DisplayRole:
{
QString str = node->value().toString();
if(str.isNull())return "NULL";
else return str;
}
default:
return QVariant();
}
}
bool DatabaseTree::canFetchMore(const QModelIndex &parent) const
{
if(!parent.isValid())
return false;
TreeNode *node = static_cast<TreeNode*>(parent.internalPointer());
//qDebug() << "Can Fetch more" << node->value();
if(node)
return !node->filled();
return false;
}
void DatabaseTree::fetchMore(const QModelIndex &parent)
{
if(!parent.isValid())
return;
TreeNode *node = static_cast<TreeNode*>(parent.internalPointer());
//qDebug() << "Fetch more" << node->value();
if(node)
{
fillNode(node);
if(node->childCount() > 0)
{
beginInsertRows(parent, 0, node->childCount() - 1);
endInsertRows();
}
}
}
QVariant DatabaseTree::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0)
return _keys.join('/');
return QVariant();
}
void DatabaseTree::load()
{
if(!_loaded)
{
_loaded = true;
setKeys(_keys);
}
}
QSqlQuery DatabaseTree::getGroupQuery(const QString &aggregateFunc) const
{
QStringList cols;
QString join;
QString sum;
for(int i = 0; i < _keys.size(); i++)
{
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id = h%1.id_file AND h%1.key = ?").arg(i);
if(_keys[i] == "DATE-OBS_YEAR")
cols.append(QString("STRFTIME('%Y', h%1.value)").arg(i));
else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
cols.append(QString("STRFTIME('%Y-%m', h%1.value)").arg(i));
else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
cols.append(QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i));
else
cols.append(QString("h%1.value").arg(i));
if(i == _keys.size() - 1)
{
QString tmp = aggregateFunc + "(" + cols.last() + ")";
if(aggregateFunc == "COUNT")
cols.append("COUNT(*)");
else
cols.last() = tmp;
}
}
QStringList group = cols;
group.removeLast();
QString sql = "SELECT " + cols.join(',') + " FROM fits_files AS f" + join + " GROUP BY " + group.join(',') + " ORDER BY " + cols.join(" NULLS LAST,") + " NULLS LAST";
QSqlQuery query(sql, _database->db());
for(auto &val : _keys)
{
if(val.startsWith("DATE-OBS_"))
query.addBindValue("DATE-OBS");
else
query.addBindValue(val);
}
qDebug() << "Group query" << sql;
if(!query.exec())
qWarning() << "Group query failed" << query.lastError();
return query;
}
void DatabaseTree::prepareQueries()
{
if(!_loaded)return;
_queries.clear();
QString join;
QString where;
for(int i = 0; i < _keys.size(); i++)
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id = h%1.id_file AND h%1.key = ?").arg(i);
for(int i = 0; i < _keys.size(); i++)
{
QString sql;
QString col = QString("h%1.value").arg(i);
if(_keys[i] == "DATE-OBS_YEAR")
col = QString("STRFTIME('%Y', h%1.value)").arg(i);
else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
col = QString("STRFTIME('%Y-%m', h%1.value)").arg(i);
else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
col = QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i);
sql = QString("SELECT %1 FROM fits_files AS f").arg(col) + join + where + QString(" GROUP BY %1 ORDER BY %1 NULLS LAST").arg(col);
qDebug() << "Tree query for" << _keys[i] << sql;
QSqlQuery query(sql, _database->db());
for(auto &val : _keys)
{
if(val.startsWith("DATE-OBS_"))
query.addBindValue("DATE-OBS");
else
query.addBindValue(val);
}
if(where.isEmpty())
where += QString(" WHERE %1 IS ?").arg(col);
else
where += QString(" AND %1 IS ?").arg(col);
_queries.append(std::move(query));
}
QSqlQuery files("SELECT f.file FROM fits_files AS f" + join + where + " GROUP BY f.id ORDER BY f.file", _database->db());
for(auto &val : _keys)
{
if(val.startsWith("DATE-OBS_"))
files.addBindValue("DATE-OBS");
else
files.addBindValue(val);
}
qDebug() << files.lastQuery();
_queries.append(std::move(files));
}
void DatabaseTree::fillNode(TreeNode *node)
{
if(node->filled())
return;
TreeNode *n = node;
QVariantList vals;
while(n->parent())
{
vals.prepend(n->value());
n = n->parent();
}
int level = vals.size();
if(level >= _queries.size())
{
qWarning() << "Level is too deep";
node->fill({});
return;
}
QSqlQuery &q = _queries[level];
for(int i = 0; i < level; i++)
q.bindValue(i + _keys.size(), vals[i]);
if(!q.exec())
{
qWarning() << "Failed to execute query" << q.lastError() << q.lastQuery() << q.boundValues();
node->fill({});
return;
}
QVariantList list;
while(q.next())
list.append(q.value(0));
node->fill(list);
}
DatabaseTreeView::DatabaseTreeView(Database *database, QWidget *parent) : QWidget(parent)
,_database(database)
{
QVBoxLayout *vlayout = new QVBoxLayout(this);
QHBoxLayout *hlayout = new QHBoxLayout;
_model = new DatabaseTree(database, this);
_treeView = new QTreeView(this);
_treeView->setModel(_model);
_treeView->setHeaderHidden(true);
_tableView = new CopyTableView(this);
_sqlModel = new QSqlQueryModel(this);
_tableView->setModel(_sqlModel);
QSettings settings;
QStringList filters = settings.value("databasetreeview/filters", QStringList{"OBJECT", "OBJECT/IMAGETYP", "OBJECT/IMAGETYP/FILTER", "OBJECT/IMAGETYP/FILTER/EXPTIME",
"IMAGETYP/OBJECT/IMAGETYP/FILTER/EXPTIME", "IMAGETYP/DATE-OBS_YEAR/EXPTIME"}).toStringList();
QStringList aggrFuncs = settings.value("databasetreeview/aggrFuncs", QStringList{"", "", "", "SUM", "SUM", "SUM"}).toStringList();
int selectedFilter = settings.value("databasetreeview/selectedFilter", 2).toInt();
_filters = new QComboBox(this);
for(int i = 0; i < std::min(filters.size(), aggrFuncs.size()); i++)
{
_filters->addItem(filters[i] + " " + aggrFuncs[i], QStringList{filters[i], aggrFuncs[i]});
}
_filters->setCurrentIndex(selectedFilter);
connect(_filters, &QComboBox::currentIndexChanged, this, &DatabaseTreeView::filterChanged);
filterChanged(_filters->currentIndex());
QStackedWidget *stackedWidget = new QStackedWidget(this);
stackedWidget->addWidget(_treeView);
stackedWidget->addWidget(_tableView);
QPushButton *addButton = new QPushButton(tr("Add"), this);
QPushButton *removeButton = new QPushButton(tr("Remove"), this);
QPushButton *treeTableButton = new QPushButton(tr("Tree/Table"), this);
treeTableButton->setCheckable(true);
connect(treeTableButton, &QPushButton::clicked, [stackedWidget](bool checked){
stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
hlayout->addWidget(_filters, 1);
hlayout->addWidget(addButton);
hlayout->addWidget(removeButton);
hlayout->addWidget(treeTableButton);
vlayout->addLayout(hlayout);
vlayout->addWidget(stackedWidget);
connect(_treeView, &QTreeView::activated, [this](const QModelIndex &index){
if(!_model->hasChildren(index))
{
QString path = _model->data(index).toString();
emit loadFile(path);
}
});
connect(addButton, &QPushButton::clicked, this, &DatabaseTreeView::addFilter);
connect(removeButton, &QPushButton::clicked, this, &DatabaseTreeView::removeFilter);
}
DatabaseTreeView::~DatabaseTreeView()
{
QStringList filters;
QStringList aggrFuncs;
for(int i = 0; i < _filters->count(); i++)
{
QStringList data = _filters->itemData(i).toStringList();
filters.append(data[0]);
aggrFuncs.append(data[1]);
}
QSettings settings;
settings.setValue("databasetreeview/filters", filters);
settings.setValue("databasetreeview/aggrFuncs", aggrFuncs);
settings.setValue("databasetreeview/selectedFilter", _filters->currentIndex());
}
void DatabaseTreeView::addFilter()
{
QStringList keywords = _database->getFitsKeywords();
QStringList data = _filters->currentData().toStringList();
DatabaseTreeSettings settings(data, keywords, this);
int result = settings.exec();
if(result == QDialog::Accepted)
{
QString keywords = settings.keywords();
QString aggrFunc = settings.aggregrationFunc();
QString text = keywords + " " + aggrFunc;
int idx = _filters->findText(text);
if(idx == -1)
{
_filters->addItem(text, QStringList{keywords, aggrFunc});
_filters->setCurrentText(text);
}
else
{
_filters->setCurrentIndex(idx);
}
}
}
void DatabaseTreeView::removeFilter()
{
if(_filters->count() > 1)
_filters->removeItem(_filters->currentIndex());
}
void DatabaseTreeView::filterChanged(int index)
{
QStringList data = _filters->itemData(index).toStringList();
QStringList keys = data[0].split('/');
_model->setKeys(keys);
setQuery(data[1]);
}
void DatabaseTreeView::visible(bool visible)
{
if(visible && !_loaded)
{
_loaded = true;
_model->load();
QStringList data = _filters->currentData().toStringList();
setQuery(data[1]);
}
}
void DatabaseTreeView::setQuery(const QString &func)
{
QStringList keys = _model->keys();
int i = 0;
_sqlModel->setQuery(_model->getGroupQuery(func));
if(!func.isEmpty())
{
QString tmp = func + "(" + keys.last() + ")";
if(func == "COUNT")
keys.append("COUNT");
else
keys.last() = tmp;
}
for(auto &key : keys)
_sqlModel->setHeaderData(i++, Qt::Horizontal, key);
_tableView->resizeColumnsToContents();
}
+83
View File
@@ -0,0 +1,83 @@
#ifndef DATABASETREE_H
#define DATABASETREE_H
#include <QAbstractItemModel>
#include <QComboBox>
#include <QDialog>
#include <QFont>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QTableView>
#include <QTreeView>
#include <memory>
class Database;
class TreeNode;
class DatabaseTreeSettings : public QDialog
{
Q_OBJECT
public:
explicit DatabaseTreeSettings(const QStringList &data, QStringList keywords, QWidget *parent = nullptr);
QString keywords() const;
QString aggregrationFunc() const;
public slots:
void acceptButton();
private:
QVector<QComboBox*> _keywordsSelect;
QComboBox *_aggregateFunction;
};
class DatabaseTree : public QAbstractItemModel
{
public:
explicit DatabaseTree(Database *database, QObject *parent = nullptr);
void setKeys(const QStringList &keys);
QStringList keys() const;
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &index) const override;
int columnCount(const QModelIndex &index) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
void load();
QSqlQuery getGroupQuery(const QString &aggregateFunc) const;
private:
void prepareQueries();
void fillNode(TreeNode *node);
Database *_database = nullptr;
std::unique_ptr<TreeNode> _rootNode;
QVector<QSqlQuery> _queries;
QStringList _keys;
QFont _italicFont;
bool _loaded = false;
};
class DatabaseTreeView : public QWidget
{
Q_OBJECT
public:
explicit DatabaseTreeView(Database *database, QWidget *parent = nullptr);
virtual ~DatabaseTreeView();
public slots:
void addFilter();
void removeFilter();
void filterChanged(int index);
void visible(bool visible);
private:
void setQuery(const QString &func);
signals:
void loadFile(const QString &file);
private:
QComboBox *_filters = nullptr;
QTreeView *_treeView = nullptr;
QTableView *_tableView = nullptr;
DatabaseTree *_model = nullptr;
QSqlQueryModel *_sqlModel = nullptr;
Database *_database = nullptr;
bool _loaded = false;
};
#endif // DATABASETREE_H
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>511</width>
<height>487</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>120</x>
<y>390</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QComboBox" name="comboBox">
<property name="geometry">
<rect>
<x>60</x>
<y>30</y>
<width>86</width>
<height>26</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit">
<property name="geometry">
<rect>
<x>180</x>
<y>30</y>
<width>113</width>
<height>26</height>
</rect>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+158 -23
View File
@@ -1,14 +1,19 @@
#include "batchprocessing.h"
#include "databaseview.h"
#include <QVBoxLayout>
#include <QPushButton>
#include <QSettings>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QSqlError>
#include <QDebug>
#include <QMenu>
#include <QClipboard>
#include <QContextMenuEvent>
#include <QDebug>
#include <QDialogButtonBox>
#include <QGuiApplication>
#include <QHeaderView>
#include <QMenu>
#include <QMimeData>
#include <QPushButton>
#include <QRegularExpression>
#include <QSettings>
#include <QSqlError>
#include <QTimer>
#include <QVBoxLayout>
#include <iostream>
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
@@ -156,32 +161,67 @@ void FITSFileModel::filesUnmarked(const QModelIndexList &indexes)
}
}
void FITSFileModel::load()
{
if(!m_loaded)
{
m_loaded = true;
prepareQuery();
}
}
void FITSFileModel::prepareQuery()
{
if(!m_loaded)return;
QString cols;
QString join;
QStringList where;
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
QVariantList bindValues;
QVariantList bindValuesJoin;
for(int i=0; i<m_value.size(); i++)
{
if(m_key[i] == "file")
where.append(QString(" f.file LIKE '%1' ").arg(m_value[i]));
{
where.append(" f.file LIKE ? ");
bindValues.append(m_value[i]);
}
else if(m_key[i] == "RA pos")
where.append(QString(" %1 BETWEEN f.minRa AND f.maxRa ").arg(RA(m_value[i])));
{
where.append(" ? BETWEEN f.minRa AND f.maxRa ");
bindValues.append(RA(m_value[i]));
}
else if(m_key[i] == "DEC pos")
where.append(QString(" %1 BETWEEN f.minDec AND f.maxDec ").arg(DEC(m_value[i])));
{
where.append(" ? BETWEEN f.minDec AND f.maxDec ");
bindValues.append(DEC(m_value[i]));
}
else if(m_key[i] == "RA range")
where.append(QString(" crVal1 BETWEEN %1 AND %2 ").arg(RA(m_value[i])).arg(RA(m_limit[i])));
{
where.append(" crVal1 BETWEEN ? AND ? ");
bindValues.append(RA(m_value[i]));
bindValues.append(RA(m_limit[i]));
}
else if(m_key[i] == "DEC range")
where.append(QString(" crVal2 BETWEEN %1 AND %2 ").arg(DEC(m_value[i])).arg(DEC(m_limit[i])));
{
where.append(" crVal2 BETWEEN ? AND ? ");
bindValues.append(DEC(m_value[i]));
bindValues.append(DEC(m_limit[i]));
}
else
join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key='%2' AND s%1.value LIKE '%3'").arg(i).arg(m_key[i]).arg(m_value[i]);
{
join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key=? AND s%1.value LIKE ? ").arg(i);
bindValuesJoin.append(m_key[i]);
bindValuesJoin.append(m_value[i]);
}
}
int i=0;
for(auto &column : m_columns)
{
cols += QString("GROUP_CONCAT(h%1.value) AS h%1_value,").arg(i);
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key='%2'").arg(i).arg(column);
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key=?").arg(i);
bindValuesJoin.append(column);
i++;
}
cols.chop(1);
@@ -190,14 +230,26 @@ void FITSFileModel::prepareQuery()
sql += join;
if(!where.isEmpty())sql += " WHERE " + where.join("AND");
sql += " GROUP BY f.id" + m_sort;
setQuery(sql);
QSqlQuery query(m_database->db());
query.prepare(sql);
for(auto &val : bindValuesJoin)
query.addBindValue(val);
for(auto &val : bindValues)
query.addBindValue(val);
if(!query.exec())
qWarning() << "Failed to exectute query" << query.lastQuery() << bindValuesJoin << bindValues;
else
setQuery(std::move(query));
setHeaderData(0, Qt::Horizontal, tr("File name"));
i = 1;
for(auto &column : m_columns)
{
setHeaderData(i++, Qt::Horizontal, column);
}
std::cout << sql.toStdString() << std::endl;
qDebug() << "DB SQL" << sql;
if(lastError().type() != QSqlError::NoError)
qDebug() << "Database error" << lastError();
@@ -205,7 +257,7 @@ void FITSFileModel::prepareQuery()
m_markedFiles = QSet<QString>(list.begin(), list.end());
}
DatabaseTableView::DatabaseTableView(QWidget *parent) : QTableView(parent)
DatabaseTableView::DatabaseTableView(QWidget *parent) : CopyTableView(parent)
{
}
@@ -214,6 +266,9 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
QMenu menu;
QAction *mark = menu.addAction(tr("Mark"));
QAction *unmark = menu.addAction(tr("Unmark"));
QAction *open = menu.addAction(tr("Open"));
QAction *openDirAction = menu.addAction(tr("Open file location"));
QAction *copyPath = menu.addAction(tr("Copy files"));
QAction *a = menu.exec(event->globalPos());
if(a == nullptr)
@@ -225,7 +280,26 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
emit filesMarked(indexes);
else if(a == unmark)
emit filesUnmarked(indexes);
else if(a == open)
emit openFile(indexes);
else if(a == openDirAction)
emit openDir(indexes);
else if(a == copyPath)
{
QStringList paths;
QList<QUrl> urls;
for(auto &index : indexes)
{
QString path = index.siblingAtColumn(0).data().toString();
paths.append(path);
urls.append(QUrl::fromLocalFile(path));
}
QMimeData *data = new QMimeData;
data->setUrls(urls);
data->setText(paths.join('\n'));
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setMimeData(data);
}
}
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
@@ -245,9 +319,12 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
m_model = new FITSFileModel(m_database, this);
QSettings settings;
m_tableView->setModel(m_model);
m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
m_tableView->setModel(m_model);
QTimer::singleShot(200, [this](){
QSettings settings;
m_tableView->horizontalHeader()->restoreState(settings.value("databaseview/header").toByteArray());
});
QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout);
@@ -270,6 +347,17 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
m_database->unmark(files);
m_model->filesUnmarked(indexes);
});
connect(m_tableView, &DatabaseTableView::openFile, [this](QModelIndexList indexes){
if(indexes.size())
emit loadFile(m_model->data(indexes.front().siblingAtColumn(0)).toString());
});
connect(m_tableView, &DatabaseTableView::openDir, [this](QModelIndexList indexes){
if(indexes.size())
{
QFileInfo info(m_model->data(indexes.front().siblingAtColumn(0)).toString());
openDir(info.absolutePath());
}
});
auto addFilterItems = [](QComboBox *combobox, const QStringList &fitsKeywords)
{
@@ -283,11 +371,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
};
QStringList fitsKeywords = m_database->getFitsKeywords();
QStringList filterKey = settings.value("databaseview/filterKey", QStringList{"file", "file", "file"}).toStringList();
for(int i=0; i<3; i++)
{
m_filterKeyword[i] = new QComboBox(this);
m_filterKeyword[i]->setMaximumWidth(300);
addFilterItems(m_filterKeyword[i], fitsKeywords);
m_filterKeyword[i]->setCurrentText(filterKey[i]);
m_search[i] = new QLineEdit(this);
m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));
@@ -307,7 +397,7 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
}
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
connect(filterButton, SIGNAL(pressed()), this, SLOT(applyFilter()));
connect(filterButton, &QPushButton::pressed, this, &DataBaseView::applyFilter);
hlayout->addWidget(filterButton);
connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
@@ -321,8 +411,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
DataBaseView::~DataBaseView()
{
QStringList filterKey;
for(int i = 0; i < 3; i++)
filterKey.append(m_filterKeyword[i]->currentText());
QSettings settings;
settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
settings.setValue("databaseview/filterKey", filterKey);
}
void DataBaseView::selectColumns()
@@ -374,7 +469,8 @@ bool DataBaseView::exportCSV(const QString &path)
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
return false;
QSqlQuery sql = m_model->query();
m_model->load();
QSqlQuery sql(m_model->query().lastQuery());
int colCount = m_model->columnCount();
QStringList header;
for(int i=0; i<colCount; i++)
@@ -402,3 +498,42 @@ bool DataBaseView::exportCSV(const QString &path)
}
return true;
}
void DataBaseView::visible(bool visible)
{
if(visible)m_model->load();
}
CopyTableView::CopyTableView(QWidget *parent) : QTableView(parent)
{
}
void CopyTableView::keyPressEvent(QKeyEvent *event)
{
if(event->matches(QKeySequence::Copy))
{
QModelIndexList list = selectedIndexes();
QString table;
if(list.size() == 0)return;
int row = list.first().row();
int col = list.first().column();
for(auto &index : list)
{
if(row != index.row())
table.append('\n');
else if(col != index.column())
table.append('\t');
table.append(index.data().toString());
row = index.row();
col = index.column();
}
qApp->clipboard()->setText(table);
event->accept();
}
else
{
QTableView::keyPressEvent(event);
}
}
+14 -1
View File
@@ -30,6 +30,7 @@ class FITSFileModel : public QSqlQueryModel
QStringList m_limit;
QSet<QString> m_markedFiles;
Database *m_database;
bool m_loaded = false;
public:
explicit FITSFileModel(Database *database, QObject *parent = nullptr);
void sort(int column, Qt::SortOrder order) override;
@@ -38,11 +39,20 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void filesMarked(const QModelIndexList &indexes);
void filesUnmarked(const QModelIndexList &indexes);
void load();
protected:
void prepareQuery();
};
class DatabaseTableView : public QTableView
class CopyTableView : public QTableView
{
Q_OBJECT
public:
explicit CopyTableView(QWidget *parent = nullptr);
void keyPressEvent(QKeyEvent *event);
};
class DatabaseTableView : public CopyTableView
{
Q_OBJECT
public:
@@ -52,6 +62,8 @@ protected:
signals:
void filesMarked(QModelIndexList indexes);
void filesUnmarked(QModelIndexList indexes);
void openFile(QModelIndexList indexes);
void openDir(QModelIndexList indexes);
};
class DataBaseView : public QWidget
@@ -72,6 +84,7 @@ public slots:
void itemActivated(const QModelIndex &index);
void applyFilter();
bool exportCSV(const QString &path);
void visible(bool visible);
signals:
void loadFile(QString file);
};
+40
View File
@@ -0,0 +1,40 @@
#ifdef FLATPAK
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusUnixFileDescriptor>
#include <QString>
#include <fcntl.h>
#include <unistd.h>
//flatpak bug prevent to use QFile::moveToTrash
bool moveToTrash(const QString &path)
{
QDBusConnection con = QDBusConnection::sessionBus();
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Trash", "TrashFile");
int fd = ::open(path.toLocal8Bit().data(), O_RDWR);
if(fd >= 0)
{
QList<QVariant> args = {QVariant::fromValue(QDBusUnixFileDescriptor(fd))};
message.setArguments(args);
QDBusMessage reply = con.call(message);
::close(fd);
if(reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() && reply.arguments().first().toInt())
return true;
else
return false;
}
return false;
}
#else
#include <QFile>
#include <QString>
bool moveToTrash(const QString &path)
{
return QFile::moveToTrash(path);
}
#endif
+766
View File
@@ -0,0 +1,766 @@
#include "filemanager.h"
#include "ui_filemanager.h"
#include "ui_fitskeyword.h"
#include <QSettings>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QMimeData>
#include <QClipboard>
#include <QThread>
#include <QDirIterator>
#include "loadimage.h"
class FileTimes
{
public:
explicit FileTimes(const QString &path)
{
QFile file(path);
#ifndef Q_OS_WIN
birthTime = file.fileTime(QFileDevice::FileBirthTime);
#endif
modificationTime = file.fileTime(QFileDevice::FileModificationTime);
accessTime = file.fileTime(QFileDevice::FileAccessTime);
}
void apply(const QString &path)
{
QFile file(path);
if(file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly))
{
#ifndef Q_OS_WIN // Only windows allow changing birth time
file.setFileTime(birthTime, QFileDevice::FileBirthTime);
#endif
file.setFileTime(accessTime, QFileDevice::FileAccessTime);
file.setFileTime(modificationTime, QFileDevice::FileModificationTime);
}
}
private:
QDateTime birthTime;
QDateTime modificationTime;
QDateTime accessTime;
};
FileTransfer::FileTransfer(FileManager *fm) :
_fm(fm)
{
}
FileTransfer::~FileTransfer()
{
_run = false;
}
void FileTransfer::copy(const QStringList &src, const QString &dst)
{
_run = true;
perform(src, dst, true);
emit finished();
}
void FileTransfer::move(const QStringList &src, const QString &dst)
{
_run = true;
perform(src, dst, false);
emit finished();
}
void FileTransfer::cancel()
{
_run = false;
}
void FileTransfer::perform(const QStringList &src, const QString &dst, bool copy)
{
QDir dstDir(dst);
if(!dstDir.exists())
{
emit error(tr("Error"), tr("Destination directory %1 doesn't exists").arg(dstDir.absolutePath()));
return;
}
QList<Action> actions;
QStringList dirs;
emit progress(0);
for(const QString &i : src)
{
QFileInfo srcInfo(i);
if(srcInfo.absolutePath() == dst || dst.startsWith(srcInfo.absoluteFilePath()))
return;
if(srcInfo.isDir())
{
QDir srcDir(i);
//qDebug() << "dir" << srcInfo.absoluteFilePath() << srcInfo.fileName();
if(!copy && !dstDir.exists(srcInfo.fileName()))
{
if(QFile::rename(srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName())))
continue;
}
actions.append({srcInfo.absoluteFilePath(), srcInfo.fileName(), true});
if(!copy)dirs.prepend(srcInfo.absoluteFilePath());
QDirIterator it(i, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while(it.hasNext())
{
QFileInfo info = it.nextFileInfo();
if(info.fileName() == "." || info.fileName() == "..")
continue;
QString relativePath = srcDir.dirName() + "/" + srcDir.relativeFilePath(info.absoluteFilePath());
if(info.isDir())
{
actions.append({"", relativePath, true});
if(!copy)dirs.prepend(info.absoluteFilePath());
//qDebug() << "dir" << info.absoluteFilePath() << relativePath;
}
else
{
actions.append({info.absoluteFilePath(), dstDir.absoluteFilePath(relativePath), false});
//qDebug() << "file" << info.absoluteFilePath() << dstDir.absoluteFilePath(relativePath);
}
}
}
else
{
actions.append({srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName())});
//qDebug() << "file" << srcInfo.absoluteFilePath() << dstDir.absoluteFilePath(srcInfo.fileName());
}
}
bool overwriteAll = false;
bool skipAll = false;
int total = actions.size();
int i = 0;
for(auto &a : actions)
{
if(!_run)
return;
if(a.dir)
{
dstDir.mkpath(a.dst);
}
else
{
QFileInfo dstInfo(a.dst);
if(dstInfo.exists())
{
if(overwriteAll)
{
QFile::remove(dstInfo.absoluteFilePath());
}
else if(skipAll)
{
emit progress(i++ * 100 / total);
continue;
}
else
{
QMessageBox::StandardButton ret;
QMetaObject::invokeMethod(_fm, "overwrite", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QMessageBox::StandardButton, ret), Q_ARG(QString, dstInfo.fileName()));
switch(ret)
{
case QMessageBox::YesToAll:
overwriteAll = true;//break; is intentionally missing
case QMessageBox::Yes:
QFile::remove(dstInfo.absoluteFilePath());
break;
case QMessageBox::NoToAll:
skipAll = true;//break; is intentionally missing
case QMessageBox::No:
emit progress(i++ * 100 / total);
continue;
break;
case QMessageBox::Cancel:
return;
default:
break;
}
}
}
FileTimes t(a.src);
if(copy)
{
if(!QFile::copy(a.src, a.dst))
{
emit error(tr("Copy failed"), tr("Failed to copy file %1 to %2").arg(a.src).arg(a.dst));
return;
}
}
else
{
if(!QFile::rename(a.src, a.dst))
{
emit error(tr("Move failed"), tr("Failed to move file %1 to %2").arg(a.src).arg(a.dst));
return;
}
}
t.apply(a.dst);
}
emit progress(i++ * 100 / total);
}
if(!copy)
{
for(const QString &d : dirs)
{
QDir dir(d);
if(dir.isEmpty())
dir.removeRecursively();
}
}
}
PathTabBar::PathTabBar(const QStringList &tabs) :
_tabs(tabs)
{
setTabsClosable(true);
setExpanding(false);
for(auto &t : _tabs)
{
QDir dir(t);
int i = addTab(tabName(t));
setTabToolTip(i, t);
}
connect(this, &QTabBar::currentChanged, [this](int index){
QString path = _tabs.at(index);
emit pathChanged(path);
});
connect(this, &QTabBar::tabCloseRequested, [this](int index){
if(_tabs.size() >= 2)
{
_tabs.remove(index);
removeTab(index);
}
});
connect(this, &QTabBar::currentChanged, [this](int index){
emit tabChanged(_tabs[index]);
});
}
QHBoxLayout *PathTabBar::createLayout()
{
QHBoxLayout *hlayout = new QHBoxLayout();
hlayout->addWidget(this);
hlayout->addStretch(2);
QPushButton *addButton = new QPushButton("+");
connect(addButton, &QPushButton::clicked, [this](){
QString path = _tabs[currentIndex()];
_tabs.append(path);
int i = addTab(tabName(path));
setTabToolTip(i, path);
});
hlayout->addWidget(addButton);
return hlayout;
}
const QStringList &PathTabBar::tabPaths() const
{
return _tabs;
}
QString PathTabBar::currentTabPath() const
{
int index = std::clamp(currentIndex(), 0, (int)_tabs.size());
return _tabs[index];
}
void PathTabBar::pathChanged(const QString &path)
{
QDir dir(path);
int index = currentIndex();
setTabText(index, tabName(path));
setTabToolTip(index, path);
_tabs[index] = path;
}
QString PathTabBar::tabName(const QString &path)
{
QDir dir(path);
if(dir.dirName().isEmpty())
return path;
else
return dir.dirName();
}
FITSSelection::FITSSelection(const QStringList &keywords, QWidget *parent) : QDialog(parent)
,ui(new Ui::FITSKeyword)
{
ui->setupUi(this);
connect(ui->addButton, &QPushButton::clicked, [this](){
auto item = ui->keywordList->findItems(ui->keyword->text(), Qt::MatchFixedString | Qt::MatchCaseSensitive);
if(item.size())return;
ui->keywordList->addItem(ui->keyword->text());
});
connect(ui->removeButton, &QPushButton::clicked, [this](){
auto items = ui->keywordList->selectedItems();
for(auto item : items)
delete item;
});
ui->keywordList->addItems(keywords);
}
FITSSelection::~FITSSelection()
{
delete ui;
}
QStringList FITSSelection::FITSKeywords() const
{
QStringList keywords;
for(int i = 0; i < ui->keywordList->count(); i++)
keywords.append(ui->keywordList->item(i)->text());
return keywords;
}
FileManager::FileManager(const QSet<QString> &openFilter, QWidget *parent) : QMainWindow(parent)
,ui(new Ui::FileManager)
{
ui->setupUi(this);
QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
QString picturesPath;
if(standardLocations.size())
picturesPath = standardLocations.first();
QSettings settings;
QStringList leftTabs = settings.value("filemanager/leftTabPaths", picturesPath).toStringList();
QStringList rightTabs = settings.value("filemanager/rightTabPaths", picturesPath).toStringList();
if(leftTabs.empty())leftTabs.append(picturesPath);
if(rightTabs.empty())rightTabs.append(picturesPath);
ui->leftTab->setOpenFilter(openFilter);
ui->rightTab->setOpenFilter(openFilter);
_rightTabBar = new PathTabBar(rightTabs);
ui->rightLayout->insertLayout(0, _rightTabBar->createLayout());
connect(_rightTabBar, &PathTabBar::tabChanged, ui->rightTab, &DirView::setDir);
_leftTabBar = new PathTabBar(leftTabs);
ui->leftLayout->insertLayout(0, _leftTabBar->createLayout());
connect(_leftTabBar, &PathTabBar::tabChanged, ui->leftTab, &DirView::setDir);
connect(ui->leftTab, &DirView::dirChanged, ui->leftPath, &QLineEdit::setText);
connect(ui->leftTab, &DirView::dirChanged, _leftTabBar, &PathTabBar::pathChanged);
connect(ui->rightTab, &DirView::dirChanged, ui->rightPath, &QLineEdit::setText);
connect(ui->rightTab, &DirView::dirChanged, _rightTabBar, &PathTabBar::pathChanged);
connect(ui->leftTab, &DirView::openFile, this, &FileManager::openFile);
connect(ui->rightTab, &DirView::openFile, this, &FileManager::openFile);
connect(ui->leftTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection);
connect(ui->rightTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection);
connect(ui->actionLoad_FITS_keywordsLeft, &QAction::toggled, ui->leftTab, &DirView::loadFitsKeywords);
connect(ui->actionLoad_FITS_keywordsRight, &QAction::toggled, ui->rightTab, &DirView::loadFitsKeywords);
ui->leftTab->setDir(_leftTabBar->currentTabPath());
ui->leftTab->setFITSKeywords(settings.value("filemanager/leftFitsKeywords", QStringList("OBJECT")).toStringList());
ui->leftTab->header()->restoreState(settings.value("filemanager/leftTabHeader").toByteArray());
ui->rightTab->setDir(_rightTabBar->currentTabPath());
ui->rightTab->setFITSKeywords(settings.value("filemanager/rightFitsKeywords", QStringList("OBJECT")).toStringList());
ui->rightTab->header()->restoreState(settings.value("filemanager/rightTabHeader").toByteArray());
ui->actionLoad_FITS_keywordsLeft->setChecked(settings.value("filemanager/leftLoadFitsKeywords", true).toBool());
ui->actionLoad_FITS_keywordsRight->setChecked(settings.value("filemanager/rightLoadFitsKeywords", true).toBool());
restoreGeometry(settings.value("filemanager/geometry").toByteArray());
setAttribute(Qt::WA_DeleteOnClose);
connect(ui->actionSelect_columnsLeft, &QAction::triggered, this, &FileManager::selectFITSKeywords);
connect(ui->actionSelect_columnsRight, &QAction::triggered, this, &FileManager::selectFITSKeywords);
connect(ui->actionCopySelectedFilesPathsLeft, &QAction::triggered, this, &FileManager::copySelectedFilesPaths);
connect(ui->actionCopySelectedFilesPathsRight, &QAction::triggered, this, &FileManager::copySelectedFilesPaths);
connect(ui->leftPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited);
connect(ui->rightPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited);
QFileInfoList drives = QDir::drives();
for(auto &drive : drives)
{
QString path = drive.absoluteFilePath();
ui->menuLeft_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->leftTab->setDir(path); });
ui->menuRight_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->rightTab->setDir(path); });
}
ui->progressBar->hide();
ui->cancelButton->hide();
_thread = new QThread(this);
_thread->start();
_fileTransfer = new FileTransfer(this);
_fileTransfer->moveToThread(_thread);
connect(_fileTransfer, &FileTransfer::progress, ui->progressBar, &QProgressBar::setValue);
connect(_fileTransfer, &FileTransfer::error, this, &FileManager::errorMessage);
connect(_fileTransfer, &FileTransfer::finished, [this](){
ui->leftTab->setDragEnabled(true);
ui->rightTab->setDragEnabled(true);
ui->progressBar->hide();
ui->cancelButton->hide();
});
connect(this, &FileManager::copy, _fileTransfer, &FileTransfer::copy);
connect(this, &FileManager::move, _fileTransfer, &FileTransfer::move);
connect(ui->cancelButton, &QPushButton::clicked, [this](){ _fileTransfer->cancel(); });
}
FileManager::~FileManager()
{
QSettings settings;
settings.setValue("filemanager/leftFitsKeywords", ui->leftTab->FITSKeywords());
settings.setValue("filemanager/leftTabPaths", _leftTabBar->tabPaths());
settings.setValue("filemanager/leftTabHeader", ui->leftTab->header()->saveState());
settings.setValue("filemanager/rightFitsKeywords", ui->rightTab->FITSKeywords());
settings.setValue("filemanager/rightTabPaths", _rightTabBar->tabPaths());
settings.setValue("filemanager/rightTabHeader", ui->rightTab->header()->saveState());
settings.setValue("filemanager/leftLoadFitsKeywords", ui->actionLoad_FITS_keywordsLeft->isChecked());
settings.setValue("filemanager/rightLoadFitsKeywords", ui->actionLoad_FITS_keywordsRight->isChecked());
settings.setValue("filemanager/geometry", saveGeometry());
delete ui;
_fileTransfer->cancel();
_thread->quit();
_thread->wait();
delete _fileTransfer;
}
void FileManager::selectFITSKeywords()
{
QStringList columns;
if(sender() == ui->actionSelect_columnsLeft)
columns = ui->leftTab->FITSKeywords();
if(sender() == ui->actionSelect_columnsRight)
columns = ui->rightTab->FITSKeywords();
FITSSelection selection(columns, this);
int ret = selection.exec();
if(ret == QDialog::Accepted)
{
if(sender() == ui->actionSelect_columnsLeft)
ui->leftTab->setFITSKeywords(selection.FITSKeywords());
if(sender() == ui->actionSelect_columnsRight)
ui->rightTab->setFITSKeywords(selection.FITSKeywords());
}
}
void FileManager::copySelectedFilesPaths()
{
if(sender() == ui->actionCopySelectedFilesPathsLeft)
ui->leftTab->copySelectedFilesPathsToClipboard();
if(sender() == ui->actionCopySelectedFilesPathsRight)
ui->rightTab->copySelectedFilesPathsToClipboard();
}
void FileManager::pathEdited()
{
if(sender() == ui->leftPath)
{
QDir dir(ui->leftPath->text());
if(dir.exists())
ui->leftTab->setDir(dir.absolutePath());
}
if(sender() == ui->rightPath)
{
QDir dir(ui->rightPath->text());
if(dir.exists())
ui->rightTab->setDir(dir.absolutePath());
}
}
void FileManager::copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst)
{
ui->leftTab->setDragEnabled(false);
ui->rightTab->setDragEnabled(false);
ui->progressBar->show();
ui->cancelButton->show();
switch(action)
{
case Qt::CopyAction:
emit copy(src, dst);
break;
case Qt::MoveAction:
emit move(src, dst);
break;
case Qt::LinkAction:
default:
break;
}
}
QMessageBox::StandardButton FileManager::overwrite(const QString &dst)
{
QMessageBox::StandardButton button = QMessageBox::question(this, tr("Overwrite file?"), tr("Destination file %1 already exists. Overwrite?").arg(dst),
QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel);
return button;
}
void FileManager::errorMessage(const QString &title, const QString &text)
{
QMessageBox::critical(this, title, text);
}
QCache<QString, ImageInfoData>* DirFileSystemModel::getCacheInstance()
{
static bool init = true;
static QCache<QString, ImageInfoData> cache;
if(!init)
{
cache.setMaxCost(10000);
init = false;
}
return &cache;
}
DirFileSystemModel::DirFileSystemModel(QWidget *parentWidget) : QFileSystemModel(parentWidget)
,_parentWidget(parentWidget)
{
_cache = getCacheInstance();
setFilter(QDir::AllEntries | QDir::NoDot);
_fitsKeywords = {"OBJECT"};
}
void DirFileSystemModel::setDir(const QString &path)
{
_dir = index(path);
}
QString DirFileSystemModel::dir() const
{
return fileInfo(_dir).canonicalFilePath();
}
void DirFileSystemModel::setFITSKeywords(const QStringList &keywords)
{
beginResetModel();
_fitsKeywords = keywords;
endResetModel();
}
const QStringList &DirFileSystemModel::FITSKeywords() const
{
return _fitsKeywords;
}
Qt::ItemFlags DirFileSystemModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags ret = QFileSystemModel::flags(index) & ~Qt::ItemIsEditable;
if(index.row() == 0)ret &= ~Qt::ItemIsDragEnabled;
return ret;
}
int DirFileSystemModel::columnCount(const QModelIndex &parent) const
{
return QFileSystemModel::columnCount(parent) + _fitsKeywords.size();
}
QVariant DirFileSystemModel::data(const QModelIndex &index, int role) const
{
if(index.column() >= QFileSystemModel::columnCount() && role == Qt::DisplayRole)
{
QFileInfo info = fileInfo(index);
QString path = info.canonicalFilePath();
QString suffix = info.suffix();
ImageInfoData *infoData = nullptr;
if(_cache->contains(path))
{
infoData = _cache->object(path);
}
else
{
if(_loadFitsKeywords)
{
infoData = new ImageInfoData;
if(isFITS(suffix))
readFITSHeader(path, *infoData);
else if(isXISF(suffix))
readXISFHeader(path, *infoData);
_cache->insert(path, infoData);
}
}
if(infoData)
{
int column = index.column() - QFileSystemModel::columnCount();
if(column < _fitsKeywords.size())
{
const QString &key = _fitsKeywords.at(column);
for(auto &record : infoData->fitsHeader)
if(record.key == key)
return record.value;
}
}
return "";
}
return QFileSystemModel::data(index, role);
}
QVariant DirFileSystemModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= QFileSystemModel::columnCount())
return _fitsKeywords.at(section - QFileSystemModel::columnCount());
return QFileSystemModel::headerData(section, orientation, role);
}
bool DirFileSystemModel::hasChildren(const QModelIndex &parent) const
{
if(parent.parent() == _dir)return false;
return QFileSystemModel::hasChildren(parent);
}
bool DirFileSystemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
Q_UNUSED(row);
Q_UNUSED(column);
if(data->hasUrls())
{
QStringList srcPaths;
for(auto &url : data->urls())
srcPaths.append(url.toLocalFile());
emit filesAction(action, srcPaths, filePath(parent));
}
return false;
}
void DirFileSystemModel::loadFitsKeywords(bool enable)
{
_loadFitsKeywords = enable;
}
DirView::DirView(QWidget *parent) : QTreeView(parent)
{
_dirFileSystemModel = new DirFileSystemModel(this);
_dirFileSystemModel->setRootPath(QDir::drives().first().path());
_dirFileSystemModel->setReadOnly(false);
setDragEnabled(true);
setAcceptDrops(true);
connect(_dirFileSystemModel, &DirFileSystemModel::filesAction, this, &DirView::filesAction);
setModel(_dirFileSystemModel);
setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(this, &QTreeView::doubleClicked, [this](const QModelIndex &index){
QFileInfo info = _dirFileSystemModel->fileInfo(index);
if(_dirFileSystemModel->isDir(index))
{
setDir(info.canonicalFilePath());
}
else if(info.isFile())
{
if(_openFilter.contains(info.suffix().toLower()))
emit openFile(info.absoluteFilePath());
else
QDesktopServices::openUrl(QUrl::fromLocalFile(info.absoluteFilePath()));
}
});
header()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(header(), &QHeaderView::customContextMenuRequested, this, &DirView::headerContextMenu);
}
void DirView::setDir(const QString &path)
{
QString oldPath = _dirFileSystemModel->dir();
#ifdef Q_OS_WINDOWS
const int ROOT_LEN = 3;
#else
const int ROOT_LEN = 1;
#endif
if(oldPath.left(ROOT_LEN) != path.left(ROOT_LEN))
_dirFileSystemModel->setRootPath(path.left(ROOT_LEN));
QString newPath = path;
if(!QFileInfo::exists(path))
{
QDir dir(path);
do
{
dir.setPath(QDir::cleanPath(dir.filePath("..")));
}while(!dir.exists() && !dir.isRoot());
newPath = dir.path();
}
_dirFileSystemModel->setDir(newPath);
setRootIndex(_dirFileSystemModel->index(newPath, 0));
clearSelection();
if(oldPath != newPath)emit dirChanged(newPath);
}
QString DirView::dir() const
{
return _dirFileSystemModel->dir();
}
void DirView::setOpenFilter(const QSet<QString> &openFilter)
{
_openFilter = openFilter;
}
void DirView::setFITSKeywords(const QStringList &keywords)
{
QString d = dir();
_dirFileSystemModel->setFITSKeywords(keywords);
setDir(d);
}
const QStringList &DirView::FITSKeywords() const
{
return _dirFileSystemModel->FITSKeywords();
}
void DirView::headerContextMenu(const QPoint &pos)
{
QHeaderView *head = header();
QMenu menu;
int count = head->count();
for(int i = 0; i < count; i++)
{
QAction *a = menu.addAction(head->model()->headerData(i, Qt::Horizontal).toString());
a->setCheckable(true);
a->setChecked(!head->isSectionHidden(i));
a->setData(i);
}
QAction *a = menu.exec(mapToGlobal(pos));
if(a)
{
if(a->isChecked())head->showSection(a->data().toInt());
else head->hideSection(a->data().toInt());
}
}
void DirView::loadFitsKeywords(bool enable)
{
_dirFileSystemModel->loadFitsKeywords(enable);
}
void DirView::copySelectedFilesPathsToClipboard() const
{
QList<QUrl> urls;
QString text;
auto selected = selectionModel()->selectedRows();
for(auto &item : selected)
{
if(item.column() == 0)
{
QString path = _dirFileSystemModel->filePath(item);
text.append(path); text.append('\n');
urls.append(QUrl::fromLocalFile(path));
}
}
QClipboard *clipboard = QApplication::clipboard();
QMimeData *mimeData = new QMimeData();
mimeData->setUrls(urls);
mimeData->setText(text);
clipboard->setMimeData(mimeData);
}
+150
View File
@@ -0,0 +1,150 @@
#ifndef FILEMANAGER_H
#define FILEMANAGER_H
#include <QMainWindow>
#include <QCache>
#include <QFileSystemModel>
#include <QTreeView>
#include <QDialog>
#include <QTabBar>
#include <QHBoxLayout>
#include <QMessageBox>
#include "imageinfodata.h"
namespace Ui {
class FileManager;
class FITSKeyword;
}
class FileManager;
class FileTransfer: public QObject
{
Q_OBJECT
public:
explicit FileTransfer(FileManager *fm);
~FileTransfer();
public slots:
void copy(const QStringList &src, const QString &dst);
void move(const QStringList &src, const QString &dst);
void cancel();
signals:
void progress(int percent);
void finished();
void error(const QString &title, const QString &text);
private:
void perform(const QStringList &src, const QString &dst, bool copy);
struct Action
{
QString src;
QString dst;
bool dir = false;
};
FileManager *_fm;
bool _run = true;
};
class PathTabBar : public QTabBar
{
Q_OBJECT
public:
explicit PathTabBar(const QStringList &tabs);
QHBoxLayout* createLayout();
const QStringList& tabPaths() const;
QString currentTabPath() const;
public slots:
void pathChanged(const QString &path);
signals:
void tabChanged(const QString &path);
private:
QStringList _tabs;
QString tabName(const QString &path);
};
class FITSSelection : public QDialog
{
Q_OBJECT
public:
FITSSelection(const QStringList &keywords, QWidget *parent = nullptr);
~FITSSelection();
QStringList FITSKeywords() const;
private:
Ui::FITSKeyword *ui;
};
class FileManager : public QMainWindow
{
Q_OBJECT
public:
explicit FileManager(const QSet<QString> &openFilter, QWidget *parent = nullptr);
~FileManager();
public slots:
void selectFITSKeywords();
void copySelectedFilesPaths();
void pathEdited();
void copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst);
QMessageBox::StandardButton overwrite(const QString &dst);
void errorMessage(const QString &title, const QString &text);
signals:
void openFile(const QString &path);
void copy(const QStringList &src, const QString &dst);
void move(const QStringList &src, const QString &dst);
private:
Ui::FileManager *ui;
PathTabBar *_leftTabBar;
PathTabBar *_rightTabBar;
QThread *_thread;
FileTransfer *_fileTransfer;
};
class DirFileSystemModel : public QFileSystemModel
{
Q_OBJECT
public:
explicit DirFileSystemModel(QWidget *parentWidget);
void setDir(const QString &path);
QString dir() const;
void setFITSKeywords(const QStringList &keywords);
const QStringList& FITSKeywords() const;
Qt::ItemFlags flags(const QModelIndex &index) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
public slots:
void loadFitsKeywords(bool enable);
signals:
void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst);
private:
mutable QCache<QString, ImageInfoData> *_cache = nullptr;
static QCache<QString, ImageInfoData>* getCacheInstance();
QModelIndex _dir;
QStringList _fitsKeywords;
bool _loadFitsKeywords = true;
QWidget *_parentWidget = nullptr;
};
class DirView : public QTreeView
{
Q_OBJECT
DirFileSystemModel *_dirFileSystemModel = nullptr;
QSet<QString> _openFilter;
public:
explicit DirView(QWidget *parent = nullptr);
void setDir(const QString &path);
QString dir() const;
void setOpenFilter(const QSet<QString> &openFilter);
void setFITSKeywords(const QStringList &keywords);
const QStringList& FITSKeywords() const;
public slots:
void headerContextMenu(const QPoint &pos);
void loadFitsKeywords(bool enable);
void copySelectedFilesPathsToClipboard() const;
signals:
void dirChanged(const QString &path);
void openFile(const QString &path);
void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst);
};
#endif // FILEMANAGER_H
+150
View File
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FileManager</class>
<widget class="QMainWindow" name="FileManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1282</width>
<height>858</height>
</rect>
</property>
<property name="windowTitle">
<string>File Manager</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="leftLayout">
<item>
<widget class="QLineEdit" name="leftPath"/>
</item>
<item>
<widget class="DirView" name="leftTab"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="rightLayout">
<item>
<widget class="QLineEdit" name="rightPath"/>
</item>
<item>
<widget class="DirView" name="rightTab"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="progressLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1282</width>
<height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuLeft_Tab">
<property name="title">
<string>Left Tab</string>
</property>
<addaction name="actionLoad_FITS_keywordsLeft"/>
<addaction name="actionSelect_columnsLeft"/>
<addaction name="actionCopySelectedFilesPathsLeft"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menuRight_Tab">
<property name="title">
<string>Right Tab</string>
</property>
<addaction name="actionLoad_FITS_keywordsRight"/>
<addaction name="actionSelect_columnsRight"/>
<addaction name="actionCopySelectedFilesPathsRight"/>
<addaction name="separator"/>
</widget>
<addaction name="menuLeft_Tab"/>
<addaction name="menuRight_Tab"/>
</widget>
<action name="actionLoad_FITS_keywordsLeft">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Load FITS keywords</string>
</property>
</action>
<action name="actionLoad_FITS_keywordsRight">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Load FITS keywords</string>
</property>
</action>
<action name="actionSelect_columnsLeft">
<property name="text">
<string>Select columns</string>
</property>
</action>
<action name="actionSelect_columnsRight">
<property name="text">
<string>Select columns</string>
</property>
</action>
<action name="actionCopySelectedFilesPathsLeft">
<property name="text">
<string>Copy selected files paths</string>
</property>
</action>
<action name="actionCopySelectedFilesPathsRight">
<property name="text">
<string>Copy selected files paths</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>DirView</class>
<extends>QTreeView</extends>
<header>filemanager.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
@@ -5,6 +5,7 @@
#include <QMenu>
#include <QSettings>
#include <QHeaderView>
#include <QMimeDatabase>
FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) : QWidget(parent)
, m_model(model)
@@ -57,7 +58,7 @@ Filetree::Filetree(QWidget *parent) : QTreeView(parent)
m_rootDir = settings.value("filetree/rootDir", QDir::homePath()).toString();
m_fileSystemModel = new FileSystemModel(this);
m_fileSystemModel->setRootPath(m_rootDir);
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.fz", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
m_fileSystemModel->setNameFilterDisables(false);
if(settings.value("filetree/showHidden", false).toBool())
m_fileSystemModel->setFilter(m_fileSystemModel->filter() | QDir::Hidden);
@@ -117,6 +118,7 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
{
setRootIndex(index);
m_rootDir = m_fileSystemModel->filePath(index);
m_fileSystemModel->setRootPath(m_rootDir);
}
else if(a == resetRoot)
{
@@ -127,6 +129,7 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
{
setRootIndex(rootIndex().parent());
m_rootDir = m_fileSystemModel->filePath(rootIndex().parent());
m_fileSystemModel->setRootPath(m_rootDir);
}
else if(a == copy)
{
@@ -3,6 +3,7 @@
#include <QWidget>
#include <QFileSystemModel>
#include <QIdentityProxyModel>
#include <QListView>
#include <QTreeView>
@@ -14,8 +15,9 @@ class FilesystemWidget : public QWidget
public:
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
void contextMenuEvent(QContextMenuEvent *event) override;
private slots:
public slots:
void selectFile(int row);
protected slots:
void fileClicked(const QModelIndex &index, const QModelIndex &);
signals:
void fileSelected(int row);
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FITSKeyword</class>
<widget class="QDialog" name="FITSKeyword">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>443</height>
</rect>
</property>
<property name="windowTitle">
<string>FITS Columns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="keywordList">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="keyword"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeButton">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>FITSKeyword</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>215</x>
<y>420</y>
</hint>
<hint type="destinationlabel">
<x>215</x>
<y>221</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>FITSKeyword</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>215</x>
<y>420</y>
</hint>
<hint type="destinationlabel">
<x>215</x>
<y>221</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+1
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <QPainter>
#include <QDebug>
#include <QStyleOption>
Histogram::Histogram(QWidget *parent) : QWidget(parent)
{
View File
+531
View File
@@ -0,0 +1,531 @@
#include "httpdownloader.h"
#include <QNetworkReply>
#include <QDebug>
#include <QRegularExpression>
#include <QFileInfo>
#ifdef PLATESOLVER
#include "solver.h"
#endif
// filename arcseconds range
// index-4119.fits 14002000
// index-4118.fits 10001400
// index-4117.fits 6801000
// index-4116.fits 480680
// index-4115.fits 340480
// index-4114.fits 240340
// index-4113.fits 170240
// index-4112.fits 120170
// index-4111.fits 85120
// index-4110.fits 60—85
// index-4109.fits 4260
// index-4108.fits 3042
// index-4107.fits 2230
// index-5206-*.fits 1622
// index-5205-*.fits 1116
// index-5204-*.fits 811
// index-5203-*.fits 5.68.0
// index-5202-*.fits 4.05.6
// index-5201-*.fits 2.84.0
static const QMap<QString, QByteArray> md5 = {
{"index-4107.fits.zst", "b4c3bc2b162fcb6417b2c3358dbf0543"},
{"index-4108.fits.zst", "14a54b8e0abcb58efb7a828fc8f00267"},
{"index-4109.fits.zst", "d6bce03dfbb527cc807ec360a8b4afa6"},
{"index-4110.fits.zst", "da0aded630ee4650850f5828b4289746"},
{"index-4111.fits.zst", "c11547481f97727e546b3b7c776f6394"},
{"index-4112.fits.zst", "fd3f5ad964d69c66555b2c5b6d65d426"},
{"index-4113.fits.zst", "4546e33817a161b8011e5f1321d39445"},
{"index-4114.fits.zst", "ebc815fa4d9a3fd259fe22b84796fbc4"},
{"index-4115.fits.zst", "5395b7b225ffe5329867354bc653887f"},
{"index-4116.fits.zst", "341cebc6b962cede0f27d08c3b3a4f23"},
{"index-4117.fits.zst", "e362a868ae0751d1a1e7f6b9e48a2f79"},
{"index-4118.fits.zst", "a7d38ec4b1d69c859e875c8d6ba1679b"},
{"index-4119.fits.zst", "9e07b46f4c4ca9ba536383d201e70c35"},
{"index-5201-00.fits.zst", "87255d073576674ec50959522cbbc9eb"},
{"index-5201-01.fits.zst", "b5154f26c8b2a6e143bdc11a062213ab"},
{"index-5201-02.fits.zst", "cf0b08e586fe2ce306adb370c9f113e8"},
{"index-5201-03.fits.zst", "eda457e3b3b419156b0cbdbe6c262fb7"},
{"index-5201-04.fits.zst", "e1344126047714aac771d37861da4698"},
{"index-5201-05.fits.zst", "1b2bf2fe61e883db7e65628761a934e8"},
{"index-5201-06.fits.zst", "e4338de4ae486cedd31ec24b2677fe1d"},
{"index-5201-07.fits.zst", "14665b88b4ab179d1bedd46acdc0d9bd"},
{"index-5201-08.fits.zst", "636f411a83dfcf0c02e13ad4c0fed948"},
{"index-5201-09.fits.zst", "8afe4edf38794225c1c3b23d72671d96"},
{"index-5201-10.fits.zst", "742db3b858e160f69f2d189961fdfcad"},
{"index-5201-11.fits.zst", "0ffb50923c71d269acc9c3c661d5429a"},
{"index-5201-12.fits.zst", "535eefd763e08593e775e0f4e19c69e3"},
{"index-5201-13.fits.zst", "e94426ba2275e76b11495105d780890d"},
{"index-5201-14.fits.zst", "754a22f37153773662acaea5ea34a417"},
{"index-5201-15.fits.zst", "7e399e94b7a15c2b97e14f49b3999070"},
{"index-5201-16.fits.zst", "7441074047de8bccd1c09570b122466d"},
{"index-5201-17.fits.zst", "bb7f5979b0d7963420dabfc5dd58407c"},
{"index-5201-18.fits.zst", "ca950e0190d849d709357bacce6fc1d0"},
{"index-5201-19.fits.zst", "36b84a8ac921064ad1a89f1155af7b31"},
{"index-5201-20.fits.zst", "25eeda073f427462e0064acf23a38498"},
{"index-5201-21.fits.zst", "0bd79e677363442dc7e994b2f088cd27"},
{"index-5201-22.fits.zst", "071abfb9131ca5a6cda792870f97bd8d"},
{"index-5201-23.fits.zst", "56721c1918e7ac114d43602ec6b17402"},
{"index-5201-24.fits.zst", "4409be2965dacf376b0124d8f7342c3c"},
{"index-5201-25.fits.zst", "e784c443787e6c3b3b51e7c82701b3b6"},
{"index-5201-26.fits.zst", "02e58904a47e3305dd2a2c1e754c2b56"},
{"index-5201-27.fits.zst", "f4f37044f787349dfda36e9aab07c348"},
{"index-5201-28.fits.zst", "69893cbd149173c98d496b3d62d23526"},
{"index-5201-29.fits.zst", "d55efc9ffca98742f7575c0fa7cd9420"},
{"index-5201-30.fits.zst", "014c94da04a6e94897af09001e08bad8"},
{"index-5201-31.fits.zst", "376319584d0b6a66bcaced5b31f705d4"},
{"index-5201-32.fits.zst", "00f2873b2468d103661e6938fed2d905"},
{"index-5201-33.fits.zst", "fa1ce3020ec8511885472c0eda777cd7"},
{"index-5201-34.fits.zst", "7c66e555866806d61f90769bc626ef32"},
{"index-5201-35.fits.zst", "f1767cf0b802a97b939711f3ecd788c8"},
{"index-5201-36.fits.zst", "76825b18fef6546bbbeef3f8538a06cb"},
{"index-5201-37.fits.zst", "af507a214fc69c7daa0688fce2924c7e"},
{"index-5201-38.fits.zst", "05fc75e562c612c51bf7bacb3907aa02"},
{"index-5201-39.fits.zst", "3eeaabf9b945d71fafff7c282f9a3add"},
{"index-5201-40.fits.zst", "f891a7def591965ad4aa4ddc9cfb7718"},
{"index-5201-41.fits.zst", "48ef1d61841567de4d94d3dc366df643"},
{"index-5201-42.fits.zst", "d2c8041bbada7df9dcc5614c35edd7f1"},
{"index-5201-43.fits.zst", "24fc923bdc21f696b1da418a131dc2bc"},
{"index-5201-44.fits.zst", "690eb483b2d60e1e31ff0e71e1c19167"},
{"index-5201-45.fits.zst", "7b7972184b9bd5d485680cb10ad7f566"},
{"index-5201-46.fits.zst", "e09515bdd779241b6871eb9130980924"},
{"index-5201-47.fits.zst", "95583b10a270336b4cfb31153305b666"},
{"index-5202-00.fits.zst", "c877e6a6790d62a77753bc0b5c1c471f"},
{"index-5202-01.fits.zst", "2069168ce477a4b9c0659eb97d9d3f3e"},
{"index-5202-02.fits.zst", "80b53bdc44addc02c5a9a47183ae405e"},
{"index-5202-03.fits.zst", "fcef358afae1ac87e1072bf94c33919f"},
{"index-5202-04.fits.zst", "fb6e067de3d8f59868fc5daad9e45ac1"},
{"index-5202-05.fits.zst", "168861bd176f0c9283ef091b855cefe8"},
{"index-5202-06.fits.zst", "c88d93502450e872004d952f5cc970c6"},
{"index-5202-07.fits.zst", "0eb1b5b3b15212f734f150087872a84c"},
{"index-5202-08.fits.zst", "03a110b7092787f0da40117d3daf4ee8"},
{"index-5202-09.fits.zst", "10b89b70f19e0042c1a832dfbb0f157c"},
{"index-5202-10.fits.zst", "6d55a5356f820b437137586037049392"},
{"index-5202-11.fits.zst", "ee561de1f6ad229b1aec1d2d576cf2d6"},
{"index-5202-12.fits.zst", "16bb2e40a0a71a91b4304c0e030d9f14"},
{"index-5202-13.fits.zst", "d6259841cb5209f1fe2262a94ebba80e"},
{"index-5202-14.fits.zst", "7fcabd9e89f560dae0ea9032817ffc95"},
{"index-5202-15.fits.zst", "42c4006c6482e6a46ed81191d03a6e54"},
{"index-5202-16.fits.zst", "a726672e54dd30367664781f533a5f48"},
{"index-5202-17.fits.zst", "67fc64ba28344d9fd31143fc5123acb3"},
{"index-5202-18.fits.zst", "97ca32bc2a0ab5313547bd01485902e1"},
{"index-5202-19.fits.zst", "d261fb13fac3aa19e930d48c6cf13929"},
{"index-5202-20.fits.zst", "7a67bc4e1d1dd003280f48815d244b52"},
{"index-5202-21.fits.zst", "bbc66dabd84be8fbb47452807aa6cbd5"},
{"index-5202-22.fits.zst", "264b65ac94678334ea5dfbc4b329f2ca"},
{"index-5202-23.fits.zst", "657492ac072d1679d77abc8f532aa2c9"},
{"index-5202-24.fits.zst", "7cbd56e15c84d8b0ad605983aa0eabcb"},
{"index-5202-25.fits.zst", "5cd3457ec29821bfca8da6da1ef76684"},
{"index-5202-26.fits.zst", "253639c9680bafbbfa465d5de51de235"},
{"index-5202-27.fits.zst", "a891918b3c22f7b1e2876358a6e971e2"},
{"index-5202-28.fits.zst", "69ee777be98231c104a2e28d2c349111"},
{"index-5202-29.fits.zst", "5b9985f33d66e4da27d4c618565f35f4"},
{"index-5202-30.fits.zst", "04d6b9acb868242cf3615ca9bef4c1d8"},
{"index-5202-31.fits.zst", "24e98426ed5a60b12a6b5652b8f68ce6"},
{"index-5202-32.fits.zst", "502ca42a47d5234aab0829a387242dfc"},
{"index-5202-33.fits.zst", "253c838df836f569afe854cf598f0c79"},
{"index-5202-34.fits.zst", "8dd8e8289e9925058c9cc11e7e76c3e3"},
{"index-5202-35.fits.zst", "5b1bb19b81633bb3c2c8d1dff4bb8507"},
{"index-5202-36.fits.zst", "7990d2b9a7f120df9095d5a93d3c94f2"},
{"index-5202-37.fits.zst", "9e5f0ff891ff1b726df0001547ffd322"},
{"index-5202-38.fits.zst", "f06244e6825e4ddb101482295b5294cb"},
{"index-5202-39.fits.zst", "390f90dae3a4124cc4c7aa157e8c8597"},
{"index-5202-40.fits.zst", "b2d380ef7974fc55f0bf31ebb62ee019"},
{"index-5202-41.fits.zst", "ae8058e144898d1b786202345b6581cf"},
{"index-5202-42.fits.zst", "1247b8a91c3a9d6b10a324247c9e02c6"},
{"index-5202-43.fits.zst", "e01049718b0c6f4eb8884c647c2cdf17"},
{"index-5202-44.fits.zst", "802f0e2d56c0e4ec3d8c6d69832102d7"},
{"index-5202-45.fits.zst", "83fe2cff3cf65317f5c1bf7b953519e9"},
{"index-5202-46.fits.zst", "f12f308a3b53d95ffd7bc420700e4f44"},
{"index-5202-47.fits.zst", "608a14303810c9762b25fc68896d2a26"},
{"index-5203-00.fits.zst", "2862efb33765b7bbefb635dcad970298"},
{"index-5203-01.fits.zst", "2cd34cef4b44ad1e770396baccb2a46c"},
{"index-5203-02.fits.zst", "40c9f67282210cc374281cde023c4e71"},
{"index-5203-03.fits.zst", "c8f40e164ec3ce1df92e3a121a127716"},
{"index-5203-04.fits.zst", "cb40c64cad1d99b55dcb0b645ae388aa"},
{"index-5203-05.fits.zst", "fe1900531baaa1bb3c513b356befd522"},
{"index-5203-06.fits.zst", "8d4b7d902bacbd478b3a372338887097"},
{"index-5203-07.fits.zst", "0f18e0822ea6b67a8e5536680df16218"},
{"index-5203-08.fits.zst", "4cd0aa9bf00f903f3c71b37e047dfd2d"},
{"index-5203-09.fits.zst", "c34aeb0674c2cbd3de31e2d9b20708f0"},
{"index-5203-10.fits.zst", "64c3f710c11b5e18743d93a1e9e204f2"},
{"index-5203-11.fits.zst", "518ee18fae552e2fd83f664028219f28"},
{"index-5203-12.fits.zst", "712d6cddc97f8c183c4d9a130ba87ca4"},
{"index-5203-13.fits.zst", "185a056e25091c23bbfa425026b9897b"},
{"index-5203-14.fits.zst", "e85ade3d5b7d1c98b5d9174fb520c154"},
{"index-5203-15.fits.zst", "182d21f53ddbec1f3585936e6463b9e8"},
{"index-5203-16.fits.zst", "c2cf948d5714d61ecb6a5e235885c5ea"},
{"index-5203-17.fits.zst", "748862d448c996eda58ede16ea37b5a3"},
{"index-5203-18.fits.zst", "d145bd1cba6ccc3948fca16fd04e7efe"},
{"index-5203-19.fits.zst", "6a031fee285d47357c3cd98148c416c7"},
{"index-5203-20.fits.zst", "d08f64480576cbcb3ce1f5625e82bc87"},
{"index-5203-21.fits.zst", "5dcec75f91802cb05c36b185ea5b26de"},
{"index-5203-22.fits.zst", "c76d7ad199114e77f4e05a290eff1de8"},
{"index-5203-23.fits.zst", "3d8909cb4322a7b7baa3cd2e464269c8"},
{"index-5203-24.fits.zst", "dd5a0ce7d08940fba606546140ccd38d"},
{"index-5203-25.fits.zst", "bd27d2e07a96d7eceb26bbfff4eaf4f4"},
{"index-5203-26.fits.zst", "8d82ba9557c9b4fea8ee1a16d1cc4bb9"},
{"index-5203-27.fits.zst", "9f7e923674521562dd54903d8102bd0c"},
{"index-5203-28.fits.zst", "3064ae36821a24d67b8c53000a6b67bc"},
{"index-5203-29.fits.zst", "4eb82a64d7c9d8f7314cfda94e160e43"},
{"index-5203-30.fits.zst", "e8cf8a17c62cf0ef09b61065d1bba527"},
{"index-5203-31.fits.zst", "488fec71fc896c780aa970228d65f749"},
{"index-5203-32.fits.zst", "8d09558d167283cf5bff4feca9202421"},
{"index-5203-33.fits.zst", "c1f61ffaaee068d0a1d1829b71f46a30"},
{"index-5203-34.fits.zst", "e2567ca06041ee6995f2cb9e282fe12b"},
{"index-5203-35.fits.zst", "1c61653eb8851385a70adb15bbb8c836"},
{"index-5203-36.fits.zst", "4d5360eea4e466121f3ffc0ad2574152"},
{"index-5203-37.fits.zst", "95b713845864aa8418af634f16a0cb84"},
{"index-5203-38.fits.zst", "7ccf07966a95072e621672dfc588d127"},
{"index-5203-39.fits.zst", "0bf97501842e571c84a90d30c4b62c45"},
{"index-5203-40.fits.zst", "f0ec8a7f888c225c749dd0ca6bb946be"},
{"index-5203-41.fits.zst", "bd6fca77c9c0aae43de799b7ad823ab5"},
{"index-5203-42.fits.zst", "71ffbc8755c943c67f8b67deda4a9d44"},
{"index-5203-43.fits.zst", "4a48b878fd510a9bde3101acd7210cdb"},
{"index-5203-44.fits.zst", "1ed3d2e05dd619d145d0aac46dd69320"},
{"index-5203-45.fits.zst", "70e4d9fb4b5d66fc24990310cfc913d4"},
{"index-5203-46.fits.zst", "ecd1d7b1cb94ba52031314d189bd2390"},
{"index-5203-47.fits.zst", "894ae74eb43a8f34ad06edea62bd4337"},
{"index-5204-00.fits.zst", "6bdb9974308249e68f1ed707d6951848"},
{"index-5204-01.fits.zst", "c10ce6a6d2375bcf3e3babced3722ecd"},
{"index-5204-02.fits.zst", "9e7ed423196691e4c9f38449957860bd"},
{"index-5204-03.fits.zst", "60ccf82d3d7443423c84c789ad5d5604"},
{"index-5204-04.fits.zst", "e8ba7567c5bda04c4fb58bb93454b8ed"},
{"index-5204-05.fits.zst", "1f36a1432c055fc96582642ea5c853b2"},
{"index-5204-06.fits.zst", "bb8a67b877eeccdfef5668796a677f4f"},
{"index-5204-07.fits.zst", "2c547a8abd2410530a7547db80c40eaa"},
{"index-5204-08.fits.zst", "5be1251fcc27f3f95c38a87ba6e0335d"},
{"index-5204-09.fits.zst", "291cbc557df140dc3caccad105f9d515"},
{"index-5204-10.fits.zst", "b155a2c52e3b5a3d99d0fa5b112cd1e4"},
{"index-5204-11.fits.zst", "0a21c7bff80b6225f00e9c2213282003"},
{"index-5204-12.fits.zst", "2ca005ea103d668ebdd2f07d215dc824"},
{"index-5204-13.fits.zst", "6a3677a3e55af336dbbaa7db29492c1d"},
{"index-5204-14.fits.zst", "00c4922987950b875ddb6d68cf22dbf2"},
{"index-5204-15.fits.zst", "4faec4fdaae6ab10d91e42f77a8786b2"},
{"index-5204-16.fits.zst", "211cb590033d680cabfa3559243bbe0f"},
{"index-5204-17.fits.zst", "f8c27d1b6448ca442b5ec13d09d161ca"},
{"index-5204-18.fits.zst", "6a264515e128f61b89a5ec94d649aa05"},
{"index-5204-19.fits.zst", "d24b3fd902dbea191953d173bf85627a"},
{"index-5204-20.fits.zst", "a240cf519935d77ebda8a1ba89629b19"},
{"index-5204-21.fits.zst", "8633a5f455a70b089916bb952649abc5"},
{"index-5204-22.fits.zst", "a64cc9fc8dc5d38d530d161dde40adb0"},
{"index-5204-23.fits.zst", "639bf9f5433a272b9208094435dfacf0"},
{"index-5204-24.fits.zst", "20eece3a49f82fe2ae575bce9bc57dc9"},
{"index-5204-25.fits.zst", "6895bc172752aa20a9975e9123d6867b"},
{"index-5204-26.fits.zst", "9cb92cd20d8060dcf8c694b670800a19"},
{"index-5204-27.fits.zst", "09894bc3185f68b49cf2eb0cc7eacebe"},
{"index-5204-28.fits.zst", "bb5a2a09b531d2ca13f341cb0b00041b"},
{"index-5204-29.fits.zst", "f2d5f146ff97b86dfb4b59c8636c69d8"},
{"index-5204-30.fits.zst", "cb8bec9885e23cce0d86a94a886858ff"},
{"index-5204-31.fits.zst", "ff92d11ee8aebd9e4cd7c63006e2ba0f"},
{"index-5204-32.fits.zst", "5bc007791035420ab06a8a8dee13f50b"},
{"index-5204-33.fits.zst", "98305f6ec87af98d0a7fb82f6cb38397"},
{"index-5204-34.fits.zst", "5af466f48514b9bec75e877e3aa348e7"},
{"index-5204-35.fits.zst", "f84d32ef9278e2fa0aa013334ebddedc"},
{"index-5204-36.fits.zst", "5e7afe529e949d83812c15ca66e5fbe4"},
{"index-5204-37.fits.zst", "091d775d07623d86adf0c6f0d61da00f"},
{"index-5204-38.fits.zst", "61c3b59cd6614357da8427887fa1d7be"},
{"index-5204-39.fits.zst", "ad5687f7e7e6d65c25f52696a5be73fb"},
{"index-5204-40.fits.zst", "d95ca1f3d0abe527518ad3c4797e3b69"},
{"index-5204-41.fits.zst", "4d49cd25ea1cf1c348916b39f026a6e1"},
{"index-5204-42.fits.zst", "7f517937c94d9db3d7515cadb5cd3b10"},
{"index-5204-43.fits.zst", "f3d336795a32af76d61742c9a29bfb14"},
{"index-5204-44.fits.zst", "3372d5b85a802f891acdadfc65e05893"},
{"index-5204-45.fits.zst", "7e6c52552bf25c63af732ec7243d8766"},
{"index-5204-46.fits.zst", "5ce56079d24213af35d9ec730e12121f"},
{"index-5204-47.fits.zst", "d33d355ad900766c8fcdd53522124d01"},
{"index-5205-00.fits.zst", "d95511d75f6915caed5a4cf010e51056"},
{"index-5205-01.fits.zst", "53857e19e4ff54360ed9335c35d20ac8"},
{"index-5205-02.fits.zst", "f8bdcd851d44da92a4a90bc71deb0782"},
{"index-5205-03.fits.zst", "4782fc867bd02c58140daecc7a4f9cab"},
{"index-5205-04.fits.zst", "b63b9bfdda4a85e9377b512038aa9627"},
{"index-5205-05.fits.zst", "ffb688a56d6dc70842765a7e1fdc9ca7"},
{"index-5205-06.fits.zst", "8d906365279b2f41baa7fedd76683619"},
{"index-5205-07.fits.zst", "82ec7cc676c9ef825f218fceb236d216"},
{"index-5205-08.fits.zst", "29171a06fd40f5c5df6e637550bc7626"},
{"index-5205-09.fits.zst", "dca0e789c482eef07bee53100e10f73a"},
{"index-5205-10.fits.zst", "7d66c8c27198481c587c1432275feced"},
{"index-5205-11.fits.zst", "ce66e30646b02e7128a004cda4240b6d"},
{"index-5205-12.fits.zst", "6a42dcd534efb467a0a53c69a6047866"},
{"index-5205-13.fits.zst", "950331af7d668da1006c1b6902fd6439"},
{"index-5205-14.fits.zst", "ac6c30027cd93e91b5baf6e344032254"},
{"index-5205-15.fits.zst", "3c8d77076a49d3dc051089df8025308b"},
{"index-5205-16.fits.zst", "5c7a0c57f7bf6fcc886c7518adc2b882"},
{"index-5205-17.fits.zst", "6daa68b68104426b3e92a433107e565e"},
{"index-5205-18.fits.zst", "74b94ab3f7ee6a260560b5d78614df30"},
{"index-5205-19.fits.zst", "01597167da7a9e6fde3ace7d6e9c6788"},
{"index-5205-20.fits.zst", "3dffc55b7ab5c15e1c689c0d73f880f6"},
{"index-5205-21.fits.zst", "bfa484d631819e2a2b7a8d3dec337a9b"},
{"index-5205-22.fits.zst", "de2a3ebbf56bb640411e0a50ed0653eb"},
{"index-5205-23.fits.zst", "5498579da779e625617140b04a88659c"},
{"index-5205-24.fits.zst", "41589963565a4d1d056ac2551c94bc5b"},
{"index-5205-25.fits.zst", "88dac5e97a8e3cccd4962ee9d1f062fb"},
{"index-5205-26.fits.zst", "528044ec968e08a1347f97d2d58bc9f8"},
{"index-5205-27.fits.zst", "50890dbe9394c9101138f781394a62da"},
{"index-5205-28.fits.zst", "da541fb011826588a7ff682d3fc1065f"},
{"index-5205-29.fits.zst", "96873ae405bd9ac727656d4fbf3c508c"},
{"index-5205-30.fits.zst", "e320dad418e7e64bbd4700e074af97b6"},
{"index-5205-31.fits.zst", "5cb52c69ad1a9b780dddd82da4295f01"},
{"index-5205-32.fits.zst", "af2f00cbfc50a82138f01ee26b9e9d91"},
{"index-5205-33.fits.zst", "0e3abcccf8295f99b846e69e0a82ee55"},
{"index-5205-34.fits.zst", "02220a844210cbab3dbf32f15f25d6ff"},
{"index-5205-35.fits.zst", "21380e2a86b908f5cef98cd5b2ba5fc5"},
{"index-5205-36.fits.zst", "5898e4e3b3f4961420124fe23c106e7e"},
{"index-5205-37.fits.zst", "12a7eebcfcb9871366f27bab7bd7c02b"},
{"index-5205-38.fits.zst", "ad7ae57547afae6d7e7b5bfde0f2dd4e"},
{"index-5205-39.fits.zst", "ce92be215ddb055395db6ff1469a13c5"},
{"index-5205-40.fits.zst", "21f0f02bf765bea7577e9c379cc32aaf"},
{"index-5205-41.fits.zst", "d7bb45a9cc162262cf860554cb577cb3"},
{"index-5205-42.fits.zst", "923d46b2900879a7deb9c07a71a5a604"},
{"index-5205-43.fits.zst", "12036538e03f7e87e7e5197a176bfbeb"},
{"index-5205-44.fits.zst", "763625ff1a99a09010b4d29ee26c45f5"},
{"index-5205-45.fits.zst", "515b596b4ccb4684d84ac5bae00c3ec7"},
{"index-5205-46.fits.zst", "69c36255820c21846ce066ef9727ad9c"},
{"index-5205-47.fits.zst", "7208bf7057c156f68f8797055279c396"},
{"index-5206-00.fits.zst", "ec763f6717dc23aa74f0c37d37bbc79d"},
{"index-5206-01.fits.zst", "77c60eb07dca413177f265fd3a7358d1"},
{"index-5206-02.fits.zst", "7b04e7e1bdd5d10a7ecd8784458dfe3a"},
{"index-5206-03.fits.zst", "ff096041f96c1a928583277d53b70754"},
{"index-5206-04.fits.zst", "edfab290c5d79b16142e8e29b930276e"},
{"index-5206-05.fits.zst", "01842535f9cd6cabebdbc99eba0c2469"},
{"index-5206-06.fits.zst", "8c191abe714e0e2c709bf1b3f1e46534"},
{"index-5206-07.fits.zst", "221bc2471617105004d213b44238fa41"},
{"index-5206-08.fits.zst", "c04330df6a106b55618cc0d0467c349d"},
{"index-5206-09.fits.zst", "ac6d28cad4716da936f5f9878ecab761"},
{"index-5206-10.fits.zst", "bd79f130d1931d167a2a9cf801b05cfd"},
{"index-5206-11.fits.zst", "2e50c634e80b32ca13d643e0535a37c1"},
{"index-5206-12.fits.zst", "c132774b1cb656056d04e8175948559f"},
{"index-5206-13.fits.zst", "bc491d7a0a773f9e499b9f18f4cc2d26"},
{"index-5206-14.fits.zst", "9209a393a7341d08982925936d587178"},
{"index-5206-15.fits.zst", "416f3f4c655fdc56504030442d52f21b"},
{"index-5206-16.fits.zst", "9e6f16e687376c17c15d3f2bb7621b8f"},
{"index-5206-17.fits.zst", "4f131eff7aa8eee019dd081a250f15bd"},
{"index-5206-18.fits.zst", "7535d000a0d9ef54c1e50202319f2a4d"},
{"index-5206-19.fits.zst", "2a20fb3cf2f2bd39c9d8f0efa376e5ea"},
{"index-5206-20.fits.zst", "0e08f721f97341a0f737b4d9ffc1bafc"},
{"index-5206-21.fits.zst", "aa2b2719031262219b9e105853655d84"},
{"index-5206-22.fits.zst", "c4966b370e8e0abe7c0827712419b63f"},
{"index-5206-23.fits.zst", "baff2c6b458965754a33c2b8e4ddae30"},
{"index-5206-24.fits.zst", "f9c37d9dadf7b5c10e417909b89dd0f6"},
{"index-5206-25.fits.zst", "c420b02c3f701459762ffd24d3ee0b7f"},
{"index-5206-26.fits.zst", "f8c296fe490a6449233787f7b2275f7d"},
{"index-5206-27.fits.zst", "b63de1ef274c7b3482ec49b038c95e4a"},
{"index-5206-28.fits.zst", "0e91227a868e4d626d05f8556fd385db"},
{"index-5206-29.fits.zst", "010af2760055eb0b0f139f26808d3d0a"},
{"index-5206-30.fits.zst", "bb75c13afb642f8d1039627885591adf"},
{"index-5206-31.fits.zst", "9cbec1344ba47dd477d6d8a1f680527e"},
{"index-5206-32.fits.zst", "74e832eb93be5e6d58793418253c1b1c"},
{"index-5206-33.fits.zst", "303d2fecce3f69914d2aec9b137cd65b"},
{"index-5206-34.fits.zst", "b2fa4d6404f11552dd0ae3212a893813"},
{"index-5206-35.fits.zst", "18533d28093a1b01ba0a17811237c9c2"},
{"index-5206-36.fits.zst", "7f8a6ce0c1e0fe7998a047172bee9390"},
{"index-5206-37.fits.zst", "eef8a7d8de31e0865de6a2563cd54602"},
{"index-5206-38.fits.zst", "27d0395d69aa600d2c334664ee88191d"},
{"index-5206-39.fits.zst", "455e2ee060c16a560e62df0bf6790027"},
{"index-5206-40.fits.zst", "9d7f12a0adfb97d7d3b904bf6f8788c4"},
{"index-5206-41.fits.zst", "d05556c1d3c15f0cdb72363a82ab6d7c"},
{"index-5206-42.fits.zst", "99268fca7e2a9161f4f1c144b13dea3a"},
{"index-5206-43.fits.zst", "e3b6becbf0949d9c40dac1d366805493"},
{"index-5206-44.fits.zst", "eb6802ea492c8ab920699a47cd8e5ccf"},
{"index-5206-45.fits.zst", "d3f692ee8ee9d6c9d3483818f2b81584"},
{"index-5206-46.fits.zst", "aff3a7ba7140e5e850c1395fba6402c0"},
{"index-5206-47.fits.zst", "27b479b738a7cd3379e105638b1fc43e"}
};
Download::Download(QNetworkReply *reply, const QString indexPath, QObject *parent) : QObject(parent)
,_reply(reply)
,_hash(QCryptographicHash::Md5)
{
connect(_reply, &QNetworkReply::finished, this, &Download::finished);
connect(_reply, &QNetworkReply::readyRead, this, &Download::readData);
connect(_reply, &QNetworkReply::downloadProgress, this, &Download::progress);
QString filename = _reply->url().fileName();
filename.remove(QRegularExpression("\\.zst$"));
_fw.setFileName(indexPath + "/" + filename);
if(_fw.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
qDebug() << "open file" << _fw.fileName();
_dstream = ZSTD_createDStream();
}
else
{
qWarning() << "Failed to open file" << _fw.fileName();
abort();
}
}
Download::~Download()
{
if(_dstream)
ZSTD_freeDStream(_dstream);
}
void Download::abort()
{
_reply->abort();
}
void Download::readData()
{
QByteArray data = _reply->readAll();
decompress(data);
}
void Download::finished()
{
if(_reply->error() == QNetworkReply::NoError)
{
QByteArray data = _reply->readAll();
qDebug() << "finished" << data.size();
decompress(data);
if(md5.contains(_reply->url().fileName()))
{
if(_hash.result().toHex() == md5[_reply->url().fileName()])
qDebug() << "DOWNLOAD OK";
else
{
qDebug() << "DOWNLOAD BAD";
_fw.remove();
return;
}
}
_fw.flush();
_fw.close();
}
else
{
qDebug() << "Failed to perform http request" << _reply->url();
_fw.remove();
}
}
void Download::decompress(QByteArray &data)
{
if(data.isEmpty() || _dstream == nullptr)return;
_hash.addData(data);
ZSTD_inBuffer inBuffer = {data.constData(), static_cast<size_t>(data.size()), 0};
QByteArray outData(ZSTD_DStreamOutSize(), '\0');
while(inBuffer.pos < inBuffer.size)
{
ZSTD_outBuffer outBuffer = {outData.data(), static_cast<size_t>(outData.size()), 0};
size_t ret = ZSTD_decompressStream(_dstream, &outBuffer, &inBuffer);
if(ZSTD_isError(ret))
{
qDebug() << "decompress error" << ZSTD_getErrorName(ret);
_fw.remove();
_reply->abort();
break;
}
else if(outBuffer.pos)
{
_fw.write(static_cast<char*>(outBuffer.dst), outBuffer.pos);
}
}
}
HttpDownloader::HttpDownloader(QObject *parent) : QObject(parent)
,_manager(new QNetworkAccessManager(this))
{
_manager->setAutoDeleteReplies(true);
connect(_manager, &QNetworkAccessManager::finished, this, &HttpDownloader::finished);
#ifdef PLATESOLVER
QDir dir(Solver::getTenmonIndexPath());
if(!dir.exists())
{
if(dir.mkpath("."))
qDebug() << "Failed to create astrometry directory";
}
_indexPath = dir.absolutePath();
#endif
}
void HttpDownloader::download(const QUrl &url)
{
if(!_queue.contains(url))
_queue.enqueue(url);
if(!_download)
finished();
}
bool HttpDownloader::downloadIndex(int scale)
{
if(scale > 19 || scale < 1)
return false;
QUrl url("https://tenmon.nouspiro.space/");
QStringList files = indexFileNames(scale);
for(auto &file : files)
{
if(QFile::exists(_indexPath + "/" + file))
{
qDebug() << "File already exists, skipping" << file;
}
else
{
url.setPath("/astrometry/" + file + ".zst");
download(url);
}
}
return true;
}
void HttpDownloader::abort()
{
if(_download)
_download->abort();
}
QStringList HttpDownloader::indexFileNames(int scale)
{
QStringList ret;
if(scale >= 7)
{
ret.append(QString("index-%1.fits").arg(4100 + scale));
}
else
{
for(int i=0; i<48; i++)
ret.append(QString("index-%1-%2.fits").arg(5200 + scale).arg(i, 2, 10, QChar('0')));
}
return ret;
}
void HttpDownloader::finished()
{
if(_queue.isEmpty())
{
_download = nullptr;
}
else
{
QUrl url = _queue.dequeue();
QString filename = url.fileName();
filename.remove(QRegularExpression("\\.zst$"));
QFileInfo info(_indexPath + "/" + filename);
if(info.exists())
{
finished();
return;
}
QNetworkRequest request(url);
_download = new Download(_manager->get(request), _indexPath, this);
connect(_download, &Download::progress, this, &HttpDownloader::updateProgress);
}
}
void HttpDownloader::updateProgress(qint64 received, qint64 total)
{
emit progress((float)received / total * 100.0f, _queue.size());
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef HTTPDOWNLOADER_H
#define HTTPDOWNLOADER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QFile>
#include <QQueue>
#include <QCryptographicHash>
#include <zstd.h>
class Download : public QObject
{
Q_OBJECT
QNetworkReply *_reply = nullptr;
ZSTD_DStream *_dstream = nullptr;
QFile _fw;
QCryptographicHash _hash;
public:
Download(QNetworkReply *reply, const QString indexPath, QObject *parent);
~Download();
void abort();
public slots:
void readData();
void finished();
signals:
void progress(qint64 received, qint64 total);
protected:
void decompress(QByteArray &data);
};
class HttpDownloader : public QObject
{
Q_OBJECT
QNetworkAccessManager *_manager;
Download *_download = nullptr;
QQueue<QUrl> _queue;
QString _indexPath;
public:
explicit HttpDownloader(QObject *parent = nullptr);
void download(const QUrl &url);
// scale in range 19-1
bool downloadIndex(int scale);
void abort();
static QStringList indexFileNames(int scale);
signals:
void progress(int percent, int files);
protected slots:
void finished();
void updateProgress(qint64 received, qint64 total);
};
#endif // HTTPDOWNLOADER_H
+53
View File
@@ -0,0 +1,53 @@
#include "imageinfo.h"
#include <QSettings>
#include <QHeaderView>
QMap<QString, QColor> headerHighlight;
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
{
setColumnCount(3);
setHeaderLabels({tr("Property"), tr("Value"), tr("Comment")});
setIndentation(5);
QSettings settings;
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
setSortingEnabled(true);
header()->setSortIndicatorClearable(true);
}
ImageInfo::~ImageInfo()
{
QSettings settings;
settings.setValue("imageinfo/headerstate", header()->saveState());
}
void ImageInfo::setInfo(const ImageInfoData &info)
{
clear();
if(info.fitsHeader.size())
{
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
for(const FITSRecord &record : info.fitsHeader)
{
QTreeWidgetItem *item = new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
if(headerHighlight.contains(record.key))
{
QColor color = headerHighlight[record.key];
item->setBackground(0, color);
item->setBackground(1, color);
item->setBackground(2, color);
}
}
addTopLevelItem(fitsHeader);
}
if(info.info.size())
{
QTreeWidgetItem *infoHeader = new QTreeWidgetItem({tr("Image info")});
for(auto &item : info.info)
{
new QTreeWidgetItem(infoHeader, {item.first, item.second});
}
addTopLevelItem(infoHeader);
}
expandAll();
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef IMAGEINFO_H
#define IMAGEINFO_H
#include <QTreeWidget>
#include "imageinfodata.h"
class ImageInfo : public QTreeWidget
{
Q_OBJECT
public:
explicit ImageInfo(QWidget *parent);
~ImageInfo() override;
public slots:
void setInfo(const ImageInfoData &info);
};
#endif // IMAGEINFO_H
+575
View File
@@ -0,0 +1,575 @@
#include "imageinfodata.h"
#include <QTime>
#include <QRectF>
#include <QRegularExpression>
#include <wcslib/wcshdr.h>
#include <wcslib/wcsfix.h>
#include "database.h"
#include "libxisf.h"
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
bool FITSRecord::editable() const
{
return noEditableKey.count(key);
}
FITSRecord::FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment) :
key(key), value(value), comment(comment)
{
}
FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
{
key = record.name.c_str();
comment = record.comment.c_str();
QString string = record.value.c_str();
if(string.startsWith('\'') && string.endsWith('\''))
{
string.chop(1);
string.remove(0, 1);
}
string = string.trimmed();
bool isint;
bool isdouble;
double vald = string.toDouble(&isdouble);
long long vall = string.toLongLong(&isint);
if(isint)
value = vall;
else if(isdouble)
value = vald;
else if(string == "T" || string == "F")
value = string == "T";
else
value = string;
}
FITSRecord::FITSRecord(const LibXISF::Property &property)
{
key = property.id.c_str();
value = QString::fromStdString(property.value.toString());
comment = property.comment.c_str();
xisf = true;
}
void WCSDataT::freeWCS()
{
wcsvfree(&nwcs, &wcs);
nwcs = 0;
wcs = nullptr;
}
WCSDataT::WCSDataT(int width, int height, char *header, int nrec) :
width(width),
height(height)
{
int nreject = 0;
int status = wcspih(header, nrec, 1, 0, &nreject, &nwcs, &wcs);
if(status != 0)
{
freeWCS();
return;
}
status = cdfix(wcs);
if(status > 0 || wcs->crpix[0] == 0)
freeWCS();
}
WCSDataT::WCSDataT(int width, int height, const QVector<FITSRecord> &header) :
width(width),
height(height)
{
int status = 0;
QByteArray str;
int nrec = 1;
for(const FITSRecord &record : header)
{
if(record.key.startsWith("PV"))continue;
if(record.xisf)continue;
QByteArray rec;
rec.append(record.key.leftJustified(8, ' '));
rec.append("= ");
rec.append(record.value.toString().toLatin1());
rec.append(" / ");
rec.append(record.comment);
str.append(rec.leftJustified(80, ' ', true));
nrec++;
}
str.append(QByteArray("END").leftJustified(80));
int nreject = 0;
status = wcspih(str.data(), nrec, 1, 0, &nreject, &nwcs, &wcs);
if(status != 0)
{
freeWCS();
return;
}
status = cdfix(wcs);
if(status > 0 || wcs->crpix[0] == 0)
freeWCS();
}
WCSDataT::~WCSDataT()
{
if(wcs)
freeWCS();
}
bool WCSDataT::pixelToWorld(const QPointF &pixel, SkyPoint &point) const
{
if(!valid())return false;
double pixcrd[2] = {pixel.x(), pixel.y()};
double imgcrd[8] = {0};
double phi = 0;
double theta = 0;
double world[8] = {0};
int stat[NWCSFIX] = {0};
int status = wcsp2s(wcs, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
if(status == 0)
{
point = SkyPoint(world[0], world[1]);
return true;
}
return false;
}
bool WCSDataT::worldToPixel(const SkyPoint &point, QPointF &pixel) const
{
if(!valid())return false;
double world[2] = {point.RA(), point.DEC()};
double phi = 0;
double theta = 0;
double imgcrd[8] = {0};
double pixcrd[8] = {0};
int stat[NWCSFIX] = {0};
int status = wcss2p(wcs, 1, 2, world, &phi, &theta, imgcrd, pixcrd, stat);
if(status == 0)
{
pixel = QPointF(pixcrd[0], pixcrd[1]);
return true;
}
return false;
}
bool WCSDataT::calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const
{
if(wcs == nullptr)return false;
minRa = 1000;
maxRa = -1000;
minDec = 1000;
maxDec = -1000;
if(wcs->crval)
{
crVal1 = wcs->crval[0];
crVal2 = wcs->crval[1];
}
else
{
crVal1 = crVal2 = NAN;
}
auto update = [&](const QPointF &pixel)
{
SkyPoint point;
pixelToWorld(pixel, point);
minRa = std::min(minRa, point.RA());
maxRa = std::max(maxRa, point.RA());
minDec = std::min(minDec, point.DEC());
maxDec = std::max(maxDec, point.DEC());
};
for(int x=0; x<width; x++)
{
update(QPointF(x, 0));
update(QPointF(x, height - 1));
}
for(int y=0; y<height; y++)
{
update(QPointF(0, y));
update(QPointF(width - 1, y));
}
QPointF ncp;
QPointF scp;
QRectF s(0, 0, width - 1, height - 1);
if(worldToPixel(SkyPoint(0, 90), ncp))
{
if(s.contains(ncp))
maxDec = 90;
}
if(worldToPixel(SkyPoint(0, -90), scp))
{
if(s.contains(scp))
minDec = -90;
}
return true;
}
double hav(double x)
{
return (1.0 - std::cos(x)) * 0.5;
}
double haverSine(const SkyPoint &a, SkyPoint &b)
{
const double ToRAD = M_PI / 180.0;
double d = hav((a.DEC() - b.DEC()) * ToRAD) + std::cos(a.DEC() * ToRAD) * std::cos(b.DEC() * ToRAD) * hav((a.RA() - b.RA()) * ToRAD);
return std::acos(1.0 - 2.0 * d) * (180.0 / M_PI);
}
SkyPointScale WCSDataT::getRaDecScale() const
{
SkyPointScale ret;
pixelToWorld(QPointF(width/2.0, height/2.0), ret.point);
SkyPoint pointX;
SkyPoint pointY;
pixelToWorld(QPointF(width/2.0+1, height/2.0), pointX);
pixelToWorld(QPointF(width/2.0, height/2.0+1), pointY);
double scaleX = haverSine(ret.point, pointX) * 3600.0;
double scaleY = haverSine(ret.point, pointY) * 3600.0;
ret.scaleLow = std::min(scaleX, scaleY);
ret.scaleHigh = std::max(scaleX, scaleY);
ret.scaleValid = true;
return ret;
}
SkyPoint::SkyPoint() : ra(NAN), dec(NAN)
{
}
SkyPoint::SkyPoint(double ra, double dec) : ra(ra), dec(dec)
{
}
void SkyPoint::set(double ra, double dec)
{
this->ra = ra;
this->dec = dec;
}
QString SkyPoint::toString() const
{
if(std::isnan(ra) || std::isnan(dec))
return QString();
QTime t(0, 0);
t = t.addSecs(ra * 240);
double deg, min, sec;
min = std::abs(std::modf(dec, &deg) * 60);
sec = std::modf(min, &min) * 60;
return QString("RA: %1 DEC: %2° %3' %4\"").arg(t.toString("HH'h' mm'm' ss's'")).arg(deg, 2, 'f', 0, '0').arg(min, 2, 'f', 0, '0').arg(sec, 2, 'f', 0, '0');
}
QString SkyPoint::RAString() const
{
return toHMS(ra / 15);
}
QString SkyPoint::DECString() const
{
return toDMS(dec);
}
double SkyPoint::fromHMS(const QString &hms)
{
double deg = fromDMS(hms);
if(std::isnan(deg))return deg;
return deg * 15.0;
}
double SkyPoint::fromDMS(const QString &dms)
{
double deg = 0.0;
QString str = dms.trimmed();
str.remove(QRegularExpression("[hdms°'\"]"));
str.replace(':', ' ');
str.replace(QRegularExpression("\\s+"), " ");
QStringList fields = str.split(' ');
double sign = 1.0;
bool ok = false;
if(fields.size() >= 1)
deg = fields.at(0).toDouble(&ok);
if(!ok)return NAN;
if(deg < 0.0)
sign = -1.0;
if(fields.size() >= 2)
deg += sign * fields.at(1).toDouble() / 60.0;
if(fields.size() >= 3)
deg += sign * fields.at(2).toDouble() / 3600.0;
return deg;
}
QString SkyPoint::toHMS(double decHour)
{
double h,m,s,md;
md = std::modf(decHour, &h) * 60.0;
s = std::modf(md, &m) * 60.0;
return QString("%1h %2m %3s").arg((int)h, 2, 10, QChar('0')).arg((int)m, 2, 10, QChar('0')).arg((int)s, 2, 10, QChar('0'));
}
QString SkyPoint::toDMS(double deg)
{
int sign = deg < 0.0 ? -1 : 1;
deg *= sign;
double d,m,s,md;
md = std::modf(deg, &d) * 60.0;
s = std::modf(md, &m) * 60.0;
return QString("%1˚ %2' %3\"").arg((int)d * sign, 2, 10, QChar('0')).arg((int)m, 2, 10, QChar('0')).arg((int)s, 2, 10, QChar('0'));
}
SkyPoint SkyPoint::operator+(const SkyPoint &p)
{
SkyPoint ret;
ret.ra = ra + p.ra;
ret.dec = dec + p.dec;
return ret;
}
SkyPointScale ImageInfoData::getCenterRaDec() const
{
SkyPointScale ret;
if(wcs && wcs->valid())
{
ret = wcs->getRaDecScale();
}
else
{
double ra,dec,focalLen,scale,pixSizeX,pixSizeY;
int binX = 1;
int binY = 1;
ra = dec = focalLen = scale = pixSizeX = pixSizeY = NAN;
bool ok;
for(const FITSRecord &header : fitsHeader)
{
if(header.key == "OBJCTRA")
{
double tmp = SkyPoint::fromHMS(header.value.toString());
if(!std::isnan(tmp))ra = tmp;
}
else if(header.key == "RA" && std::isnan(ra))
{
double tmp = header.value.toDouble(&ok);
if(ok)ra = tmp;
}
else if(header.key == "OBJCTDEC")
{
double tmp = SkyPoint::fromDMS(header.value.toString());
if(!std::isnan(tmp))dec = tmp;
}
else if(header.key == "DEC" && std::isnan(dec))
{
double tmp = SkyPoint::fromDMS(header.value.toString());
if(!std::isnan(tmp))dec = tmp;
}
else if(header.key == "SCALE")
{
double tmp = header.value.toDouble(&ok);
if(ok)scale = tmp;
}
else if(header.key == "FOCALLEN")
{
double tmp = header.value.toDouble(&ok);
if(ok)focalLen = tmp;
}
else if(header.key == "PIXSIZE1" || header.key == "XPIXSZ")
{
pixSizeX = header.value.toDouble();
}
else if(header.key == "PIXSIZE2" || header.key == "YPIXSZ")
{
pixSizeY = header.value.toDouble();
}
else if(header.key == "XBINNING")
{
int tmp = header.value.toInt(&ok);
if(ok)binX = tmp;
}
else if(header.key == "YBINNING")
{
int tmp = header.value.toInt(&ok);
if(ok)binY = tmp;
}
}
ret.point.set(ra, dec);
if(!std::isnan(scale))
{
ret.scaleLow = ret.scaleHigh = scale;
ret.scaleValid = true;
}
else if(!(std::isnan(focalLen) || std::isnan(pixSizeX) || std::isnan(pixSizeY)))
{
const double r = 206.2648097656; // (180 * 3600) / (1000 * pi) magic number to convert pixel size to focal length ratio to arcsec.
ret.scaleLow = std::min(pixSizeX * binX / focalLen * r, pixSizeY * binY / focalLen * r);
ret.scaleHigh = std::max(pixSizeX * binX / focalLen * r, pixSizeY * binY / focalLen * r);
ret.scaleValid = true;
}
}
if(ret.scaleValid)
{
ret.scaleLow *= 0.8;
ret.scaleHigh *= 1.2;
}
return ret;
}
SkyPoint greatCircle(SkyPoint &p, double dist, double azm)
{
dist = dist * M_PI / 180;
azm = azm * M_PI / 180;
double dec0 = p.DEC() * M_PI / 180;
double ra0 = p.RA() * M_PI / 180;
double dec1 = std::asin(std::sin(dec0) * std::cos(dist) + std::cos(dec0) * std::sin(dist) * std::cos(azm));
double ra1 = ra0 + std::atan2(std::sin(azm) * std::sin(dist) * std::cos(dec0), std::cos(dist) - std::sin(dec0) * std::sin(dec1));
return SkyPoint(ra1 * 180 / M_PI, dec1 * 180 / M_PI);
}
SkyGrid WCSDataT::prepareGrid(uint32_t w, uint32_t h, Database *database)
{
SkyGrid skyGrid;
if(!wcs)return skyGrid;
double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
QPointF a,b;
worldToPixel(SkyPoint(crVal1, crVal2), a);
worldToPixel(SkyPoint(crVal1 + 0.01, crVal2), b);
skyGrid.rot_ang = std::atan2(b.y() - a.y(), b.x() - a.x()) / M_PI * -180.0;
if(database)
{
double size = std::max(maxRa - minRa, maxDec - minDec);
skyGrid.objects = database->getObjects(minRa - size, maxRa + size, minDec - size, maxDec + size);
for(auto &object : skyGrid.objects)
{
QPointF p;
if(worldToPixel(object.skyPoint, p))
object.pixel = p;
QPointF majax;
worldToPixel(greatCircle(object.skyPoint, (object.min_ax + object.maj_ax) / 120.0, object.pos_ang), majax);
majax -= p;
object.maj_ax = std::sqrt(QPointF::dotProduct(majax, majax));
}
}
double raStep = 15;
double decStep = 15;
double raRange = maxRa - minRa;
double decRange = maxDec - minDec;
const QVector<double> raSteps = {15, 5, 2.5, 1.25, 0.25, 20/240.0, 10/240.0, 5/240.0, 1/240.0};
const QVector<double> decSteps = {20, 10, 5, 2, 1, 20/60.0, 10/60.0, 5/60.0, 2/60.0, 1/60.0, 20/3600.0, 10/3600.0, 5/3600.0, 2/3600.0, 1/3600.0};
for(double ra : raSteps)
{
if(ra * 5 <= raRange)
{
raStep = ra;
break;
}
}
for(double dec : decSteps)
{
if(dec * 5 <= decRange)
{
decStep = dec;
break;
}
}
minRa -= std::fmod(minRa, raStep);
minDec -= std::fmod(minDec, decStep);
if(minRa < 0)minRa -= raStep;
if(minDec < 0)minDec -= decStep;
QRectF clip(0, 0, w, h);
const double step = 0.2;
maxRa += raStep;
maxDec += decStep;
for(double ra = minRa; ra <= maxRa; ra += raStep)
{
QPointF p;
worldToPixel(SkyPoint(ra, minDec), p);
skyGrid.grid.moveTo(p);
for(double dec = minDec + decStep * step; dec <= maxDec; dec += decStep * step)
{
worldToPixel(SkyPoint(ra, dec), p);
skyGrid.grid.lineTo(p);
}
}
for(double dec = minDec; dec <= maxDec; dec += decStep)
{
QPointF p;
worldToPixel(SkyPoint(minRa, dec), p);
skyGrid.grid.moveTo(p);
for(double ra = minRa + raStep * step; ra <= maxRa; ra += raStep * step)
{
worldToPixel(SkyPoint(ra, dec), p);
skyGrid.grid.lineTo(p);
}
}
SkyPoint sp1, sp2,orig;
pixelToWorld(QPointF(-1, -1), orig);
sp1 = orig;
for(uint32_t x = 0; x < w; x++)
{
QPointF p(x, 0);
if(!pixelToWorld(p, sp2))
break;
if(static_cast<int>(sp1.RA() / raStep) != static_cast<int>(sp2.RA() / raStep))
skyGrid.text.append({p, std::abs(sp1.RA()) > std::abs(sp2.RA()) ? sp1.RAString() : sp2.RAString()});
if(static_cast<int>(sp1.DEC() / decStep) != static_cast<int>(sp2.DEC() / decStep))
skyGrid.text.append({p, std::abs(sp1.DEC()) > std::abs(sp2.DEC()) ? sp1.DECString() : sp2.DECString()});
sp1 = sp2;
}
sp1 = orig;
for(uint32_t y = 0; y < h; y++)
{
QPointF p(0, y);
if(!pixelToWorld(p, sp2))
break;
if(static_cast<int>(sp1.RA() / raStep) != static_cast<int>(sp2.RA() / raStep))
skyGrid.text.append({p, std::abs(sp1.RA()) > std::abs(sp2.RA()) ? sp1.RAString() : sp2.RAString()});
if(static_cast<int>(sp1.DEC() / decStep) != static_cast<int>(sp2.DEC() / decStep))
skyGrid.text.append({p, std::abs(sp1.DEC()) > std::abs(sp2.DEC()) ? sp1.DECString() : sp2.DECString()});
sp1 = sp2;
}
skyGrid.empty = false;
return skyGrid;
}
void SkyGrid::clear()
{
empty = true;
grid.clear();
text.clear();
objects.clear();
}
+122
View File
@@ -0,0 +1,122 @@
#ifndef IMAGEINFODATA_H
#define IMAGEINFODATA_H
#include <QString>
#include <QPointF>
#include <QVector>
#include <QVariant>
#include <QPainterPath>
#include <wcslib/wcs.h>
#include <cmath>
#include <memory>
namespace LibXISF { struct FITSKeyword; struct Property; }
class Database;
struct FITSRecord
{
QByteArray key;
QVariant value;
QByteArray comment;
bool xisf = false;
bool editable() const;
FITSRecord(){}
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
FITSRecord(const LibXISF::FITSKeyword &record);
FITSRecord(const LibXISF::Property &property);
};
class SkyPoint
{
double ra = NAN;
double dec = NAN;
public:
SkyPoint();
SkyPoint(double ra, double dec);
void set(double ra, double dec);
double RA() const { return ra; }
double RAHour() const { return ra / 15.0; }
double DEC() const { return dec; }
QString toString() const;
QString RAString() const;
QString DECString() const;
static double fromHMS(const QString &hms);
static double fromDMS(const QString &dms);
static QString toHMS(double decHour);
static QString toDMS(double deg);
SkyPoint operator+(const SkyPoint &p);
};
struct SkyPointScale
{
SkyPoint point;
//arcsec per pixel
bool scaleValid = false;
double scaleLow = 0.0;
double scaleHigh = 10000.0;
};
struct SkyObject
{
QString name;
QString name2;
SkyPoint skyPoint;
double maj_ax;
double min_ax;
double pos_ang;
double mag;
QPointF pixel;
};
struct SkyGrid
{
bool empty = true;
QPainterPath grid;
QVector<QPair<QPointF, QString>> text;
QVector<SkyObject> objects;
double rot_ang = 0;
void clear();
};
class WCSDataT
{
int nwcs = 0;
struct wcsprm *wcs = nullptr;
int width;
int height;
void freeWCS();
public:
WCSDataT(int width, int height, char *header, int nrec);
WCSDataT(int width, int height, const QVector<FITSRecord> &header);
WCSDataT(const WCSDataT &) = delete;
~WCSDataT();
bool pixelToWorld(const QPointF &pixel, SkyPoint &point) const;
bool worldToPixel(const SkyPoint &point, QPointF &pixel) const;
bool calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const;
bool valid() const { return wcs; };
SkyPointScale getRaDecScale() const;
SkyGrid prepareGrid(uint32_t w, uint32_t h, Database *database);
};
struct ImageInfoData
{
QVector<FITSRecord> fitsHeader;
QVector<QPair<QString, QString>> info;
std::shared_ptr<WCSDataT> wcs;
SkyPointScale getCenterRaDec() const;
int index = 0;
int num = 1;
};
typedef enum
{
None,
Statistics,
Peaks,
Stars,
}AnalyzeLevel;
Q_DECLARE_METATYPE(ImageInfoData);
#endif // IMAGEINFODATA_H
+133 -30
View File
@@ -4,6 +4,7 @@
#include <QDir>
#include <QSettings>
#include <QTimer>
#include <QRegularExpression>
#include "loadrunable.h"
#include "rawimage.h"
#include "database.h"
@@ -22,13 +23,16 @@ Image::Image(const QString name, int number, ImageRingList *ringList) :
{
}
void Image::load()
void Image::load(int index, QThreadPool *pool)
{
if(index != m_info.index && !m_loading)
m_rawImage.reset();
if(!m_rawImage && !m_loading)
{
m_loading = true;
m_released = false;
QThreadPool::globalInstance()->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel()));
pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel(), index));
}
if(!m_loading && m_rawImage)
emit pixmapLoaded(this);
@@ -37,7 +41,7 @@ void Image::load()
void Image::loadThumbnail(QThreadPool *pool)
{
if(!m_thumbnail)
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, true));
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, 0, true));
else
emit thumbnailLoaded(this);
}
@@ -84,6 +88,11 @@ void Image::clearThumbnail()
m_thumbnail.reset();
}
bool Image::isLoading() const
{
return m_loading;
}
void Image::imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info)
{
m_loading = false;
@@ -102,26 +111,35 @@ void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
emit thumbnailLoaded(this);
}
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractItemModel(parent)
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractListModel(parent)
, m_liveMode(false)
, m_analyzeLevel(None)
, m_database(database)
, m_nameFilter(nameFilter)
, m_fileSuffix(nameFilter)
{
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
connect(&m_fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &ImageRingList::dirChanged);
m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
m_loadPool = new QThreadPool(this);
m_loadPool->setThreadPriority(QThread::LowPriority);
m_thumbPool = new QThreadPool(this);
m_thumbPool->setThreadPriority(QThread::LowPriority);
m_slideShowTimer = new QTimer(this);
connect(m_slideShowTimer, &QTimer::timeout, this, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
m_dirChangeDelay = new QTimer(this);
m_dirChangeDelay->setInterval(3000);
m_dirChangeDelay->setSingleShot(true);
connect(m_dirChangeDelay, &QTimer::timeout, this, &ImageRingList::reloadDir);
}
ImageRingList::~ImageRingList()
{
QThreadPool::globalInstance()->clear();
m_loadPool->clear();
m_thumbPool->clear();
QThreadPool::globalInstance()->waitForDone();
m_loadPool->waitForDone();
m_thumbPool->waitForDone();
}
@@ -131,6 +149,7 @@ bool ImageRingList::setDir(const QString path, const QString &currentFile, bool
if(dir.exists())
{
m_currentDir = path;
QStringList scannedDirs;
QStringList absolutePaths;
std::function<void(const QString&)> scanDir = [&](const QString &path)
@@ -156,9 +175,10 @@ bool ImageRingList::setDir(const QString path, const QString &currentFile, bool
};
scanDir(path);
qDebug() << absolutePaths.size();
setFiles(absolutePaths, m_liveMode ? absolutePaths.first() : currentFile);
//qDebug() << absolutePaths.size();
setFilesPrivate(absolutePaths, m_liveMode ? absolutePaths.first() : currentFile);
if(m_fileSystemWatcher.directories().size())
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
m_fileSystemWatcher.addPath(path);
return true;
@@ -168,11 +188,25 @@ bool ImageRingList::setDir(const QString path, const QString &currentFile, bool
void ImageRingList::setFile(const QString &file)
{
if(!file.isEmpty())
{
QFileInfo info(file);
if(info.isDir())
setDir(file, QString(), true);
else
setDir(info.absolutePath(), file);
}
}
void ImageRingList::setFiles(QStringList files)
{
QRegularExpression reg("(" + m_fileSuffix.join("|") + ")");
files.removeIf([&reg](const QString &file){
QFileInfo info(file);
auto match = reg.match(info.suffix());
return !match.hasMatch() || !info.exists() || !info.isReadable() || !info.isFile();
});
setFilesPrivate(files);
}
ImagePtr ImageRingList::currentImage()
@@ -183,20 +217,25 @@ ImagePtr ImageRingList::currentImage()
return 0;
}
QString ImageRingList::currentDir() const
{
return m_currentDir;
}
void ImageRingList::increment()
{
if(m_images.size())
{
//don't increment if current image was not loaded yet
if(!(*m_currImage)->rawImage())
if((*m_currImage)->isLoading())
return;
(*m_firstImage)->release();
m_firstImage = increment(m_firstImage);
m_currImage = increment(m_currImage);
(*m_currImage)->load();
(*m_currImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load();
(*m_lastImage)->load(0, m_loadPool);
}
}
@@ -204,12 +243,64 @@ void ImageRingList::decrement()
{
if(m_images.size())
{
//don't decrement if current image was not loaded yet
if((*m_currImage)->isLoading())
return;
(*m_lastImage)->release();
m_firstImage = decrement(m_firstImage);
m_currImage = decrement(m_currImage);
(*m_currImage)->load();
(*m_currImage)->load(0, m_loadPool);
m_lastImage = decrement(m_lastImage);
(*m_firstImage)->load();
(*m_firstImage)->load(0, m_loadPool);
}
}
void ImageRingList::prevSubImage()
{
if(m_images.size())
{
if((*m_currImage)->isLoading())
return;
int index = (*m_currImage)->info().index;
int num = (*m_currImage)->info().num;
if(num > 1)
(*m_currImage)->load(index == 0 ? num - 1 : index - 1, m_loadPool);
}
}
void ImageRingList::nextSubImage()
{
if(m_images.size())
{
if((*m_currImage)->isLoading())
return;
int index = (*m_currImage)->info().index;
int num = (*m_currImage)->info().num;
if(num > 1)
(*m_currImage)->load((index + 1) % num, m_loadPool);
}
}
void ImageRingList::setMarked()
{
QStringList files = m_database->getMarkedFiles();
files.removeIf([](const QString &file){
QFileInfo info(file);
return !info.exists() || !info.isReadable();
});
setFilesPrivate(files);
}
void ImageRingList::reloadImage()
{
if(*m_currImage)
{
int index = (*m_currImage)->info().index;
(*m_currImage)->release();
(*m_currImage)->load(index, m_loadPool);
}
}
@@ -253,7 +344,7 @@ void ImageRingList::loadFile(int row)
if(m_images.empty())
return;
(*m_currImage)->load();
(*m_currImage)->load(0, m_loadPool);
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
if(m_liveMode)
@@ -262,9 +353,9 @@ void ImageRingList::loadFile(int row)
for(int i=0; i<m_width; i++)
{
m_firstImage = decrement(m_firstImage);
(*m_firstImage)->load();
(*m_firstImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load();
(*m_lastImage)->load(0, m_loadPool);
}
if(m_lastImage != m_firstImage)
{
@@ -321,7 +412,7 @@ void ImageRingList::clearThumbnails()
img->clearThumbnail();
}
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
/*QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
{
Q_UNUSED(parent);
return createIndex(row, column, m_images.at(row).get());
@@ -331,7 +422,7 @@ QModelIndex ImageRingList::parent(const QModelIndex &child) const
{
Q_UNUSED(child);
return QModelIndex();
}
}*/
int ImageRingList::rowCount(const QModelIndex &parent) const
{
@@ -341,14 +432,16 @@ int ImageRingList::rowCount(const QModelIndex &parent) const
return 0;
}
int ImageRingList::columnCount(const QModelIndex &parent) const
/*int ImageRingList::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
}*/
QVariant ImageRingList::data(const QModelIndex &index, int role) const
{
if(index.isValid() && index.row() >= 0 && index.row() < m_images.size())
{
switch(role)
{
case Qt::DisplayRole:
@@ -366,6 +459,8 @@ QVariant ImageRingList::data(const QModelIndex &index, int role) const
default:
return QVariant();
}
}
return QVariant();
}
QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int role) const
@@ -388,9 +483,9 @@ void ImageRingList::setPreload(int width)
for(int i = newWidth - m_width; i>0; i--)
{
m_firstImage = decrement(m_firstImage);
(*m_firstImage)->load();
(*m_firstImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load();
(*m_lastImage)->load(0, m_loadPool);
}
}
if(newWidth < m_width)
@@ -443,11 +538,11 @@ void ImageRingList::toggleSlideshow(bool start)
}
}
void ImageRingList::setFiles(const QStringList files, const QString &currentFile)
void ImageRingList::setFilesPrivate(const QStringList files, const QString &currentFile)
{
QThreadPool::globalInstance()->clear();
m_loadPool->clear();
m_thumbPool->clear();
QThreadPool::globalInstance()->waitForDone();
m_loadPool->waitForDone();
m_thumbPool->waitForDone();
beginResetModel();
m_images.clear();
@@ -455,8 +550,8 @@ void ImageRingList::setFiles(const QStringList files, const QString &currentFile
for(const QString &file : files)
{
ImagePtr ptr = make_shared<Image>(file, i++, this);
connect(ptr.get(), SIGNAL(pixmapLoaded(Image*)), this, SLOT(imageLoaded(Image*)));
connect(ptr.get(), SIGNAL(thumbnailLoaded(Image*)), this, SIGNAL(thumbnailLoaded(Image*)));
connect(ptr.get(), &Image::pixmapLoaded, this, &ImageRingList::imageLoaded);
connect(ptr.get(), &Image::thumbnailLoaded, this, &ImageRingList::thumbnailLoaded);
m_images.append(ptr);
}
@@ -495,14 +590,22 @@ void ImageRingList::imageLoaded(Image *image)
}
}
void ImageRingList::dirChanged(QString dir)
void ImageRingList::dirChanged(QString)
{
if(m_liveMode)
reloadDir();
else
m_dirChangeDelay->start();
}
void ImageRingList::reloadDir()
{
QString currentFile;
if(m_images.size())
currentFile = (*m_currImage)->name();
setDir(dir, currentFile);
setDir(m_currentDir, currentFile);
if(m_images.size())
emit currentImageChanged(m_currImage-m_images.begin());
}
+21 -8
View File
@@ -7,8 +7,9 @@
#include <QPixmap>
#include <QDir>
#include <memory>
#include "imageinfo.h"
#include "imageinfodata.h"
#include "rawimage.h"
#include <QAbstractItemModel>
class ImageRingList;
class QThreadPool;
@@ -27,7 +28,7 @@ class Image : public QObject
ImageRingList *m_ringList;
public:
explicit Image(const QString name, int number, ImageRingList *ringList);
void load();
void load(int index, QThreadPool *pool);
void loadThumbnail(QThreadPool *pool);
void release();
QString name() const;
@@ -37,6 +38,7 @@ public:
bool isCurrent() const;
int number() const;
void clearThumbnail();
bool isLoading() const;
signals:
void pixmapLoaded(Image *ptr);
void thumbnailLoaded(Image *ptr);
@@ -49,7 +51,7 @@ typedef std::shared_ptr<Image> ImagePtr;
class Database;
class ImageRingList : public QAbstractItemModel
class ImageRingList : public QAbstractListModel
{
Q_OBJECT
int m_width;
@@ -62,16 +64,22 @@ class ImageRingList : public QAbstractItemModel
QDir::SortFlag m_sort = QDir::Name;
bool m_reversed = false;
AnalyzeLevel m_analyzeLevel;
QThreadPool *m_loadPool;
QThreadPool *m_thumbPool;
Database *m_database;
QStringList m_nameFilter;
QStringList m_fileSuffix;
QTimer *m_slideShowTimer;
QTimer *m_dirChangeDelay;
QString m_currentDir;
public:
explicit ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent = 0);
~ImageRingList() override;
bool setDir(const QString path, const QString &currentFile = QString(), bool recursive = false);
void setFile(const QString &file);
void setFiles(QStringList files);
ImagePtr currentImage();
QString currentDir() const;
void setLiveMode(bool live);
void setCalculateStats(bool stats);
void setFindPeaks(bool findPeaks);
@@ -85,10 +93,10 @@ public:
void updateMark();
void clearThumbnails();
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
//QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
//QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
//int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
public slots:
@@ -98,8 +106,12 @@ public slots:
void toggleSlideshow(bool start);
void increment();
void decrement();
void prevSubImage();
void nextSubImage();
void setMarked();
void reloadImage();
protected:
void setFiles(const QStringList files, const QString &currentFile = QString());
void setFilesPrivate(const QStringList files, const QString &currentFile = QString());
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
QList<ImagePtr>::iterator decrement(QList<ImagePtr>::iterator iter);
signals:
@@ -109,7 +121,8 @@ signals:
void currentImageChanged(int index);
protected slots:
void imageLoaded(Image *image);
void dirChanged(QString dir);
void dirChanged(QString);
void reloadDir();
};
#endif // IMAGERINGLIST_H
+158
View File
@@ -0,0 +1,158 @@
#include "imagescrollarea.h"
#include "imageringlist.h"
#include <QDebug>
#include <QKeyEvent>
#include <QGridLayout>
#include <QMimeData>
#include <QMessageBox>
#include <QCoreApplication>
#include <QPainter>
#include <QFileInfo>
#include <QScrollBar>
#include <cmath>
ImageScrollArea::ImageScrollArea(Database *database, QWidget *parent) : QWidget(parent)
{
QGridLayout *layout = new QGridLayout(this);
setLayout(layout);
ImageWidgetGL *imageWidgetGL = new ImageWidgetGL(database, this);
m_imageWidget = imageWidgetGL;
m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
layout->setSpacing(0);
layout->addWidget(dynamic_cast<ImageWidgetGL*>(m_imageWidget), 0, 0);
layout->addWidget(m_verticalScrollBar, 0, 1);
layout->addWidget(m_horizontalScrollBar, 1, 0);
connect(m_verticalScrollBar, &QScrollBar::valueChanged, this, &ImageScrollArea::scrollEvent);
connect(m_horizontalScrollBar, &QScrollBar::valueChanged, this, &ImageScrollArea::scrollEvent);
if(imageWidgetGL)
{
connect(imageWidgetGL, &ImageWidgetGL::fileDropped, this, &ImageScrollArea::fileDropped);
connect(imageWidgetGL, &ImageWidgetGL::status, this, &ImageScrollArea::status);
connect(imageWidgetGL, &ImageWidgetGL::scrollBarsUpdate, this, &ImageScrollArea::updateScrollbars);
}
}
ImageScrollArea::~ImageScrollArea()
{
}
void ImageScrollArea::allocateThumbnails(const QStringList &paths)
{
m_imageWidget->allocateThumbnails(paths);
}
void ImageScrollArea::showThumbnail(bool enable)
{
m_imageWidget->showThumbnail(enable);
}
void ImageScrollArea::setBayerMask(int mask)
{
m_imageWidget->setBayerMask(mask);
}
void ImageScrollArea::setColormap(int colormap)
{
m_imageWidget->setColormap(colormap);
}
void ImageScrollArea::updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV)
{
if(maxH > 0)
{
m_horizontalScrollBar->show();
m_horizontalScrollBar->setRange(0, maxH);
m_horizontalScrollBar->setPageStep(stepH);
m_horizontalScrollBar->setValue(valueH);
}
else
m_horizontalScrollBar->hide();
if(maxV > 0)
{
m_verticalScrollBar->show();
m_verticalScrollBar->setRange(0, maxV);
m_verticalScrollBar->setPageStep(stepV);
m_verticalScrollBar->setValue(valueV);
}
else
m_verticalScrollBar->hide();
}
void ImageScrollArea::zoomIn()
{
m_imageWidget->zoom(1);
}
void ImageScrollArea::zoomOut()
{
m_imageWidget->zoom(-1);
}
void ImageScrollArea::bestFit()
{
m_horizontalScrollBar->hide();
m_verticalScrollBar->hide();
m_imageWidget->bestFit();
}
void ImageScrollArea::oneToOne()
{
m_imageWidget->zoom(0);
}
void ImageScrollArea::imageLoaded(Image *image)
{
if(image)
{
m_imageWidget->setImage(image->rawImage(), image->number());
m_imageWidget->setWCS(image->info().wcs);
}
}
void ImageScrollArea::thumbnailLoaded(const Image *image)
{
m_imageWidget->thumbnailLoaded(image);
}
void ImageScrollArea::setMTFParams(const MTFParam &params)
{
m_imageWidget->setMTFParams(params);
}
void ImageScrollArea::invert(bool enable)
{
m_imageWidget->invert(enable);
}
void ImageScrollArea::superPixel(bool enable)
{
m_imageWidget->superPixel(enable);
}
void ImageScrollArea::falseColor(bool enable)
{
m_imageWidget->falseColor(enable);
}
void ImageScrollArea::drawGrid(bool enable)
{
m_imageWidget->drawGrid(enable);
}
QImage ImageScrollArea::renderToImage()
{
return m_imageWidget->renderToImage();
}
void ImageScrollArea::scrollEvent()
{
m_imageWidget->setOffset(m_horizontalScrollBar->value(), m_verticalScrollBar->value());
}
+44
View File
@@ -0,0 +1,44 @@
#ifndef IMAGESCROLLAREA_H
#define IMAGESCROLLAREA_H
#include "imagewidget.h"
#include <QScrollBar>
class ImageScrollArea : public QWidget
{
Q_OBJECT
QScrollBar *m_verticalScrollBar;
QScrollBar *m_horizontalScrollBar;
ImageWidget *m_imageWidget;
public:
explicit ImageScrollArea(Database *database, QWidget *parent = nullptr);
~ImageScrollArea();
void allocateThumbnails(const QStringList &paths);
void showThumbnail(bool enable);
void setBayerMask(int mask);
void setColormap(int colormap);
protected:
void updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
public slots:
void zoomIn();
void zoomOut();
void bestFit();
void oneToOne();
void imageLoaded(Image *image);
void thumbnailLoaded(const Image *image);
void setMTFParams(const MTFParam &params);
void invert(bool enable);
void superPixel(bool enable);
void falseColor(bool enable);
void drawGrid(bool enable);
QImage renderToImage();
protected slots:
void scrollEvent();
signals:
void fileDropped(const QString &path);
void status(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
void scrollBarsUpdate(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
};
#endif // IMAGESCROLLAREA_H
+398 -188
View File
@@ -1,21 +1,23 @@
#include "imagescrollareagl.h"
#include <QOpenGLFunctions>
#include <QOpenGLVersionFunctionsFactory>
#include <QDebug>
#include <QKeyEvent>
#include <QOpenGLDebugLogger>
#include <QOpenGLPixelTransferOptions>
#include <QOpenGLFramebufferObject>
#include <QGridLayout>
#include <QMimeData>
#include <QMessageBox>
#include "imagewidget.h"
#include <QCoreApplication>
#include <QPainter>
#include <QFileInfo>
#include <cmath>
#include <QMessageBox>
#include <QTimer>
#include <QElapsedTimer>
#include <QOpenGLFramebufferObject>
#include <QOpenGLExtraFunctions>
#include <QOpenGLDebugLogger>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QPainter>
#include <QStandardPaths>
#include <QFloat16>
#include <QStyle>
#include "imageringlist.h"
int FILTERING = 1;
bool OpenGLES = false;
const int LUT_SIZE = 32;
bool BESTFIT = false;
struct RawImageType
{
@@ -31,7 +33,7 @@ RawImageType getRawImageType(const RawImage *img)
{
case RawImage::UINT8:
if(img->channels() >= 3)
type.textureFormat = QOpenGLTexture::SRGB8_Alpha8;
type.textureFormat = QOpenGLTexture::RGBA8_UNorm;
else
type.textureFormat = QOpenGLTexture::R8_UNorm;
type.dataType = QOpenGLTexture::UInt8;
@@ -50,6 +52,13 @@ RawImageType getRawImageType(const RawImage *img)
type.textureFormat = QOpenGLTexture::R32F;
type.dataType = QOpenGLTexture::Float32;
break;
case RawImage::FLOAT16:
if(img->channels() >= 3)
type.textureFormat = QOpenGLTexture::RGBA16F;
else
type.textureFormat = QOpenGLTexture::R16F;
type.dataType = QOpenGLTexture::Float16;
break;
default:
qWarning() << "Invalid format" << img->type();
break;
@@ -63,14 +72,14 @@ RawImageType getRawImageType(const RawImage *img)
return type;
}
ImageWidget::ImageWidget(Database *database, QWidget *parent) : QOpenGLWidget(parent)
ImageWidgetGL::ImageWidgetGL(Database *database, QWidget *parent) : QOpenGLWidget(parent)
, m_database(database)
{
setFocusPolicy(Qt::ClickFocus);
m_updateTimer = new QTimer(this);
m_updateTimer->setInterval(500);
m_updateTimer->setSingleShot(true);
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(update()));
connect(m_updateTimer, &QTimer::timeout, this, static_cast<void (QOpenGLWidget::*)()>(&ImageWidgetGL::update));
setAcceptDrops(true);
QTimer::singleShot(1000, [this](){
if(!isValid())
@@ -81,14 +90,15 @@ ImageWidget::ImageWidget(Database *database, QWidget *parent) : QOpenGLWidget(pa
});
setMouseTracking(true);
m_bestFit = BESTFIT;
}
ImageWidget::~ImageWidget()
ImageWidgetGL::~ImageWidgetGL()
{
makeCurrent();
}
void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
void ImageWidgetGL::setImage(std::shared_ptr<RawImage> image, int index)
{
m_currentImg = index;
@@ -105,19 +115,31 @@ void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
m_error.clear();
makeCurrent();
m_rawImage = image;
if((int)image->width() > m_maxTextureSize || (int)image->height() > m_maxTextureSize)
m_rawImage->resize(std::min(m_maxTextureSize, (int)image->width()), std::min(m_maxTextureSize, (int)image->height()));
m_imgWidth = image->width();
m_imgHeight = image->height();
bool tooBig = false;
if((int)image->width() > m_maxTextureSize || (int)image->height() > m_maxTextureSize)
{
tooBig = true;
m_swPaint = true;
}
if(!m_image)return;
RawImageType rawImageType = getRawImageType(image.get());
m_srgb = rawImageType.textureFormat == QOpenGLTexture::SRGB8_Alpha8;
m_srgb = image->getLUT().size() > 0;
m_bwImg = image->channels() == 1;
f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
if(m_srgb)
{
m_lut->setData(0, 0, 0, LUT_SIZE, LUT_SIZE, LUT_SIZE, 0, QOpenGLTexture::RGBA, QOpenGLTexture::RGBA, QOpenGLTexture::Float16, image->getLUT().data());
}
if(!tooBig)
{
while(f->glGetError() != GL_NO_ERROR);
QElapsedTimer timer;
timer.start();
m_image->destroy();
@@ -128,10 +150,13 @@ void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
m_image->allocateStorage();
m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
m_image->setWrapMode(QOpenGLTexture::ClampToEdge);
m_image->setBorderColor(0, 0, 0, 0);
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, (const void*)image->data(), m_transferOptions.get());
m_image->generateMipMaps();
qDebug() << "setImage" << timer.elapsed();
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, (const void*)image->data());
m_image->bind();
f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
f->glGenerateMipmap(GL_TEXTURE_2D);
qDebug() << "ImageWidgetGL::setImage" << timer.elapsed() << "ms";
m_swPaint = f->glGetError() != GL_NO_ERROR;
}
m_unit_scale[0] = 1.0f;
m_unit_scale[1] = 0.0f;
@@ -152,12 +177,16 @@ void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
else setOffset(m_dx, m_dy);
}
void ImageWidget::setWCS(std::shared_ptr<WCSData> wcs)
void ImageWidgetGL::setWCS(std::shared_ptr<WCSDataT> wcs)
{
m_wcs = wcs;
m_grid.clear();
if(m_drawGrid && m_wcs)
m_grid = m_wcs->prepareGrid(m_imgWidth, m_imgHeight, m_database);
}
void ImageWidget::zoom(int zoom, const QPointF &mousePos)
void ImageWidgetGL::zoom(int zoom, const QPointF &mousePos)
{
m_bestFit = false;
if(zoom != 0)
@@ -169,10 +198,10 @@ void ImageWidget::zoom(int zoom, const QPointF &mousePos)
if(!mousePos.isNull())
focus = mousePos;
if(width() > m_image->width() * m_scale)
m_dx = -width() * 0.5f + m_image->width() * m_scale * 0.5f;
if(height() > m_image->height() * m_scale)
m_dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
if(width() > m_imgWidth * m_scale)
m_dx = -width() * 0.5f + m_imgWidth * m_scale * 0.5f;
if(height() > m_imgHeight * m_scale)
m_dy = -height() * 0.5f + m_imgHeight * m_scale * 0.5f;
float newScale = std::sqrt(std::pow(2.0f, (float)m_scaleStop));
float r = newScale / m_scale;
@@ -181,20 +210,14 @@ void ImageWidget::zoom(int zoom, const QPointF &mousePos)
setOffset(m_dx * r + focus.x() * (r - 1), m_dy * r + focus.y() * (r - 1));
}
void ImageWidget::bestFit()
void ImageWidgetGL::bestFit()
{
m_bestFit = true;
m_scale = std::min((float)m_width/m_imgWidth, (float)m_height/m_imgHeight);
setOffset(0, 0);
}
void ImageWidget::blockRepaint(bool block)
{
m_blockRepaint = block;
if(!block)update();
}
void ImageWidget::allocateThumbnails(const QStringList &paths)
void ImageWidgetGL::allocateThumbnails(const QStringList &paths)
{
makeCurrent();
int count = paths.size();
@@ -208,27 +231,30 @@ void ImageWidget::allocateThumbnails(const QStringList &paths)
}
m_thumbnailTexture->destroy();
m_thumbnailTexture->create();
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
m_thumbnailTexture->setFormat(QOpenGLTexture::RGBA16F);
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
m_thumbnailTexture->setLayers(std::min((int)paths.size(), m_maxArrayLayers));
m_thumbnailTexture->setAutoMipMapGenerationEnabled(false);
m_thumbnailTexture->setWrapMode(QOpenGLTexture::ClampToEdge);
m_thumbnailTexture->setMipLevelRange(0, 0);
m_thumbnailTexture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
m_thumbnailTexture->allocateStorage();
}
QVector2D ImageWidget::getImagePixelCoord(const QVector2D &pos)
QVector2D ImageWidgetGL::getImagePixelCoord(const QVector2D &pos)
{
float dx = m_dx;
float dy = m_dy;
if(m_width > m_image->width()*m_scale)
dx = -width()*0.5f + m_image->width()*m_scale*0.5f;
if(m_height > m_image->height()*m_scale)
dy = -height()*0.5f + m_image->height()*m_scale*0.5f;
if(m_width > m_imgWidth * m_scale)
dx = -width()*0.5f + m_imgWidth*m_scale * 0.5f;
if(m_height > m_imgHeight * m_scale)
dy = -height()*0.5f + m_imgHeight*m_scale * 0.5f;
QVector2D offset(dx, dy);
return (pos + offset) / m_scale;
}
void ImageWidget::setBayerMask(int mask)
void ImageWidgetGL::setBayerMask(int mask)
{
m_firstRed[0] = mask & 0x1;
m_firstRed[1] = (mask & 0x2) >> 1;
@@ -240,13 +266,19 @@ void ImageWidget::setBayerMask(int mask)
update();
}
void ImageWidget::setMTFParams(const MTFParam &params)
void ImageWidgetGL::setColormap(int colormap)
{
m_colormapIdx = colormap;
update();
}
void ImageWidgetGL::setMTFParams(const MTFParam &params)
{
m_mtfParams = params;
update();
}
void ImageWidget::setOffset(float dx, float dy)
void ImageWidgetGL::setOffset(float dx, float dy)
{
m_dx = std::clamp(dx, 0.0f, std::max(0.0f, m_imgWidth * m_scale - m_width));
if(m_showThumbnails)
@@ -257,25 +289,25 @@ void ImageWidget::setOffset(float dx, float dy)
update();
}
void ImageWidget::superPixel(bool enable)
void ImageWidgetGL::superPixel(bool enable)
{
m_superpixel = enable;
update();
}
void ImageWidget::invert(bool enable)
void ImageWidgetGL::invert(bool enable)
{
m_invert = enable;
update();
}
void ImageWidget::falseColor(bool enable)
void ImageWidgetGL::falseColor(bool enable)
{
m_falseColor = enable;
update();
}
QImage ImageWidget::renderToImage()
QImage ImageWidgetGL::renderToImage()
{
if(m_imgWidth < 0)return QImage();
makeCurrent();
@@ -283,6 +315,7 @@ QImage ImageWidget::renderToImage()
fbo.bind();
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
m_vao->bind();
m_program->bind();
m_program->setUniformValue("viewport", (float)m_imgWidth, (float)m_imgHeight);
@@ -290,16 +323,20 @@ QImage ImageWidget::renderToImage()
m_program->setUniformValue("zoom", 1.0f);
if(m_superpixel && m_debayerTex)
{
f->glActiveTexture(GL_TEXTURE0);
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
}
else
m_image->bind(0);
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
m_vao->release();
fbo.bindDefault();
return fbo.toImage(true);
}
void ImageWidget::thumbnailLoaded(const Image *image)
void ImageWidgetGL::thumbnailLoaded(const Image *image)
{
if(image->number() >= m_maxArrayLayers)
return;
@@ -307,7 +344,9 @@ void ImageWidget::thumbnailLoaded(const Image *image)
makeCurrent();
const RawImage *raw = image->thumbnail();
if(!raw || !raw->valid())return;
m_thumbnailTexture->setData(0, image->number(), QOpenGLTexture::RGBA, QOpenGLTexture::UInt16, raw->data(), m_transferOptions.get());
f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_thumbnailTexture->setData(0, image->number(), QOpenGLTexture::RGBA, QOpenGLTexture::Float16, raw->data());
f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
float a = raw->thumbAspect();
int sizes[3] = { std::max(1, a > 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE * a)), std::max(1, a < 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE / a)), image->number() };
m_sizesDirty = true;
@@ -315,24 +354,176 @@ void ImageWidget::thumbnailLoaded(const Image *image)
if(!m_updateTimer->isActive())m_updateTimer->start();
}
void ImageWidget::showThumbnail(bool enable)
void ImageWidgetGL::showThumbnail(bool enable)
{
m_showThumbnails = enable;
setOffset(m_dx, m_dy);
}
void ImageWidget::paintGL()
void swPaint(std::shared_ptr<RawImage> &rawImage, float dx, float dy, float scale, const MTFParam &mtfParams, QWidget *widget)
{
if(m_blockRepaint)return;
QPainter painter(widget);
int width = widget->width();
int height = widget->height();
QImage img(width, height, QImage::Format_RGB32);
img.fill(Qt::darkGray);
int64_t ox = dx;
int64_t oy = dy;
auto mtf = [&mtfParams](int i, float x)
{
x = (x - mtfParams.blackPoint[i]) / (mtfParams.whitePoint[i] - mtfParams.blackPoint[i]);
x = std::clamp(x, 0.0f, 1.0f);
return ((mtfParams.midPoint[i] - 1.0f) * x) / ((2.0f * mtfParams.midPoint[i] - 1.0f) * x - mtfParams.midPoint[i]);
};
int imgWidth = rawImage->width();
int imgHeight = rawImage->height();
auto convert = [&](auto *src)
{
float s = 1.0f;
if constexpr(std::numeric_limits<std::remove_reference_t<decltype(*src)>>::is_integer)
s = (float)std::numeric_limits<std::remove_reference_t<decltype(*src)>>::max();
float iscale = 1.0f / scale;
float r[4];
float g[4];
float b[4];
for(int64_t y = std::max((int64_t)0, -oy); y < height; y++)
{
uint32_t *pixels = (uint32_t*)(img.scanLine(y));
float iptr;
float fy = std::modf((y + oy) * iscale, &iptr);
int64_t py = iptr;
int64_t w = py * rawImage->widthSamples();
int64_t w2 = w;
if(py+1 < imgHeight)w2 += rawImage->widthSamples();
if(py >= imgHeight)break;
for(int64_t x = std::max((int64_t)0, -ox); x < width; x++)
{
float fx = std::modf((x + ox) * iscale, &iptr);
int px = iptr;
int px2 = px + 1 < imgWidth ? px + 1 : px;
if(px >= imgWidth)break;
if(rawImage->channels() > 1)
{
r[0] = src[w + px * 4 + 0];
g[0] = src[w + px * 4 + 1];
b[0] = src[w + px * 4 + 2];
if(FILTERING)
{
r[1] = src[w + px2 * 4 + 0];
g[1] = src[w + px2 * 4 + 1];
b[1] = src[w + px2 * 4 + 2];
r[2] = src[w2 + px * 4 + 0];
g[2] = src[w2 + px * 4 + 1];
b[2] = src[w2 + px * 4 + 2];
r[3] = src[w2 + px2 * 4 + 0];
g[3] = src[w2 + px2 * 4 + 1];
b[3] = src[w2 + px2 * 4 + 2];
}
}
else
{
r[0] = src[w + px];
if(FILTERING)
{
r[2] = src[w2 + px];
r[1] = src[w + px2];
r[3] = src[w2 + px2];
}
}
uint32_t rgb = 0xff000000;
if(FILTERING)
{
if(rawImage->channels() > 1)
{
rgb |= (uint8_t)(mtf(0, ((r[3] * fx + r[2] * (1.0f - fx)) * fy + (r[1] * fx + r[0] * (1.0f - fx)) * (1.0f - fy)) / s) * 255.0f) << 16;
rgb |= (uint8_t)(mtf(1, ((g[3] * fx + g[2] * (1.0f - fx)) * fy + (g[1] * fx + g[0] * (1.0f - fx)) * (1.0f - fy)) / s) * 255.0f) << 8;
rgb |= (uint8_t)(mtf(1, ((b[3] * fx + b[2] * (1.0f - fx)) * fy + (b[1] * fx + b[0] * (1.0f - fx)) * (1.0f - fy)) / s) * 255.0f);
}
else
{
uint32_t v = (uint8_t)(mtf(0, ((r[3] * fx + r[2] * (1.0f - fx)) * fy + (r[1] * fx + r[0] * (1.0f - fx)) * (1.0f - fy)) / s) * 255.0f);
rgb = 0xff000000 | (v << 16) | (v << 8) | v;
}
}
else
{
if(rawImage->channels() > 1)
{
rgb |= (uint8_t)(mtf(0, r[0] / s) * 255.0f) << 16;
rgb |= (uint8_t)(mtf(1, g[0] / s) * 255.0f) << 8;
rgb |= (uint8_t)(mtf(1, b[0] / s) * 255.0f);
}
else
{
uint32_t v = (uint8_t)(mtf(0, r[0] / s) * 255.0f);
rgb = 0xff000000 | (v << 16) | (v << 8) | v;
}
}
pixels[x] = rgb;
}
}
};
if(rawImage)
{
switch(rawImage->type())
{
case RawImage::UINT8:
convert(static_cast<uint8_t*>(rawImage->data()));
break;
case RawImage::UINT16:
convert(static_cast<uint16_t*>(rawImage->data()));
break;
case RawImage::UINT32:
convert(static_cast<uint32_t*>(rawImage->data()));
break;
case RawImage::FLOAT16:
convert(static_cast<qfloat16*>(rawImage->data()));
break;
case RawImage::FLOAT32:
convert(static_cast<float*>(rawImage->data()));
break;
case RawImage::FLOAT64:
convert(static_cast<double*>(rawImage->data()));
break;
}
}
painter.drawImage(0, 0, img);
}
void ImageWidgetGL::drawGrid(bool enable)
{
if(m_grid.empty && m_wcs)
m_grid = m_wcs->prepareGrid(m_imgWidth, m_imgHeight, m_database);
if(enable != m_drawGrid)
{
m_drawGrid = enable;
update();
}
}
void ImageWidgetGL::paintGL()
{
float dx = m_dx;
float dy = m_dy;
if(m_width > m_image->width() * m_scale)
dx = -width() * 0.5f + m_image->width() * m_scale * 0.5f;
if(m_height > m_image->height() * m_scale)
dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
if(m_width > m_imgWidth * m_scale)
dx = -width() * 0.5f + m_imgWidth * m_scale * 0.5f;
if(m_height > m_imgHeight * m_scale)
dy = -height() * 0.5f + m_imgHeight * m_scale * 0.5f;
QBrush highlight = style()->standardPalette().highlight();
f->glClear(GL_COLOR_BUFFER_BIT);
f->glBlendFunc(GL_ONE, GL_ZERO);
if(m_showThumbnails)
{
m_vaoThumb->bind();
@@ -358,16 +549,20 @@ void ImageWidget::paintGL()
m_thumbnailProgram->setUniformValueArray("mtf_param", m_mtfParams.blackPoint, 3, 3);
m_thumbnailProgram->setUniformValue("invert", m_invert);
m_thumbnailProgram->setUniformValue("offset", 0, m_dy);
QMatrix4x4 mvp;
mvp.ortho(rect());
m_thumbnailProgram->setUniformValue("mvp", mvp);
if(f3)f3->glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, m_thumbnailCount);
QPainter painter(this);
const int w = width()/THUMB_SIZE_BORDER;
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
int start = std::max((int)(m_dy / THUMB_SIZE_BORDER_Y * w - w), 0);
int end = std::min((int)(m_dy + m_height) / THUMB_SIZE_BORDER_Y * w + w, m_thumbnailCount);
fx->glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, m_thumbnailCount);
m_vaoThumb->release();
QPainter painter(this);
for(int i=start; i < end; i++)
{
float x = (i % w) * THUMB_SIZE_BORDER;
@@ -402,11 +597,20 @@ void ImageWidget::paintGL()
}
else
{
if(m_swPaint)
{
swPaint(m_rawImage, dx, dy, m_scale, m_mtfParams, this);
}
else
{
m_vao->bind();
debayer();
m_vao->bind();
if(m_superpixel && m_debayerTex)
{
f->glActiveTexture(GL_TEXTURE0);
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
}
else
m_image->bind(0);
@@ -420,15 +624,51 @@ void ImageWidget::paintGL()
m_program->setUniformValue("false_color", m_falseColor && m_bwImg);
m_program->setUniformValue("invert", m_invert);
m_program->setUniformValue("filtering", m_scale > 1.0f ? FILTERING : 1);
#ifdef COLOR_MANAGMENT
m_program->setUniformValue("lut_table", 2);
m_program->setUniformValue("srgb", m_srgb);
#endif
m_program->setUniformValue("colormapIdx", m_colormapIdx);
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
m_vao->release();
if(m_drawGrid && !m_grid.empty)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
painter.setPen(QPen(Qt::yellow, 1.0 / m_scale));
QTransform tran;
tran.translate(-std::floor(dx), -std::floor(dy));
tran.scale(m_scale, m_scale);
painter.setTransform(tran);
painter.setClipRect(0, 0, m_imgWidth, m_imgHeight);
painter.drawPath(m_grid.grid);
painter.setPen(Qt::yellow);
QFont font({"Arial", "serif-sans"});
font.setPointSizeF(12 / m_scale);
painter.setFont(font);
for(auto &text : m_grid.text)
painter.drawText(QRectF(text.first, QSizeF(4000, 4000)), text.second);
painter.setPen(QPen(Qt::green, 1.0 / m_scale));
QFontMetricsF fontMetric = QFontMetricsF(font);
for(auto &object : m_grid.objects)
{
QRectF rect = fontMetric.boundingRect(object.name);
rect.moveCenter(object.pixel);
painter.setTransform(tran);
painter.drawText(rect, Qt::TextDontClip, object.name);
painter.translate(object.pixel);
painter.rotate(object.pos_ang);
painter.drawEllipse(QPointF(0, 0), object.maj_ax, object.maj_ax);
}
}
}
}
}
void ImageWidget::resizeGL(int w, int h)
void ImageWidgetGL::resizeGL(int w, int h)
{
m_width = w;
m_height = h;
@@ -437,15 +677,39 @@ void ImageWidget::resizeGL(int w, int h)
updateScrollBars();
}
void ImageWidget::initializeGL()
void ImageWidgetGL::initializeGL()
{
f = context()->functions();
fx = context()->extraFunctions();
f->glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
f3 = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_3_3_Core>(context());
if(f3 == nullptr)
if(fx == nullptr)
QMessageBox::critical(this, tr("OpenGL error"), tr("Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed."));
OpenGLES = context()->isOpenGLES();
auto loadShader = [](const QString &file)
{
QFile fr(file);
fr.open(QIODevice::ReadOnly);
QByteArray src;
if(OpenGLES)
{
src = "#version 300 es\n"
"precision highp float;\n"
"precision highp sampler2D;\n"
"precision highp sampler2DArray;\n"
"precision highp sampler3D;\n"
"#line 1\n";
}
else
{
src = "#version 330\n#line 1\n";
}
src.append(fr.readAll());
return src;
};
m_vao = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
m_vaoThumb = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
m_vao->create();
@@ -457,19 +721,19 @@ void ImageWidget::initializeGL()
logger->startLogging();
connect(logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage &message)
{
qDebug() << message;
qDebug() << "OpenGL debug" << message;
});
qDebug() << "Vendor:" << (char*)f->glGetString(GL_VENDOR);
qDebug() << "Renderer:" << (char*)f->glGetString(GL_RENDERER);
qDebug() << "Version:" << (char*)f->glGetString(GL_VERSION);
qDebug() << "OpenGL Vendor:" << (char*)f->glGetString(GL_VENDOR);
qDebug() << "OpenGL Renderer:" << (char*)f->glGetString(GL_RENDERER);
qDebug() << "OpenGL Version:" << (char*)f->glGetString(GL_VERSION);
f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_maxTextureSize);
f->glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &m_maxArrayLayers);
qDebug() << "Max texture size:" << m_maxTextureSize << "max layers:" << m_maxArrayLayers;
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
qDebug() << context()->format();
qDebug() << "OpenGL context format" << context()->format();
// each vertex is x,y 2D position and s,t texture coordinates
float vertexs[] = {-1.0f, -1.0f, 0.0f, 1.0f,
@@ -484,8 +748,8 @@ void ImageWidget::initializeGL()
// f->glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(float)*4, 0);
m_program = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/image.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/image.frag");
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, loadShader(":/image.vert"));
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, loadShader(":/image.frag"));
if(!m_program->link())
{
@@ -498,11 +762,13 @@ void ImageWidget::initializeGL()
m_program->enableAttributeArray("qt_MultiTexCoord0");
m_program->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
m_program->setUniformValue("qt_Texture0", (GLuint)0);
m_program->setUniformValue("lut_table", (GLuint)2);
m_program->setUniformValue("colormap", (GLuint)3);
m_program->setUniformValue("scale", 1.0f, 0.0f);
m_debayerProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/debayer.vert");
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/debayer.frag");
m_debayerProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, loadShader(":/debayer.vert"));
m_debayerProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, loadShader(":/debayer.frag"));
m_debayerProgram->bind();
m_debayerProgram->enableAttributeArray("qt_Vertex");
@@ -518,8 +784,8 @@ void ImageWidget::initializeGL()
m_vaoThumb->bind();
m_thumbnailProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/thumb.vert");
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/thumb.frag");
m_thumbnailProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, loadShader(":/thumb.vert"));
m_thumbnailProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, loadShader(":/thumb.frag"));
m_thumbnailProgram->bind();
m_thumbnailProgram->enableAttributeArray("qt_Vertex");
@@ -539,8 +805,9 @@ void ImageWidget::initializeGL()
m_bufferSizes->allocate(12);
m_thumbnailProgram->enableAttributeArray("imageSize_num");
f3->glVertexAttribIPointer(m_thumbnailProgram->attributeLocation("imageSize_num"), 3, GL_INT, 0, nullptr);
f3->glVertexAttribDivisor(m_thumbnailProgram->attributeLocation("imageSize_num"), 1);
fx->glVertexAttribIPointer(m_thumbnailProgram->attributeLocation("imageSize_num"), 3, GL_INT, 0, nullptr);
fx->glVertexAttribDivisor(m_thumbnailProgram->attributeLocation("imageSize_num"), 1);
m_vaoThumb->release();
m_image = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2D));
m_image->setFormat(QOpenGLTexture::RGB8U);
@@ -550,27 +817,39 @@ void ImageWidget::initializeGL()
m_image->setMagnificationFilter(QOpenGLTexture::Linear);
m_thumbnailTexture = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2DArray));
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
m_thumbnailTexture->setLayers(1);
m_thumbnailTexture->allocateStorage();
m_thumbnailTexture->bind(1);
m_thumbnailTexture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
m_transferOptions->setAlignment(1);
m_lut = std::make_unique<QOpenGLTexture>(QOpenGLTexture::Target3D);
m_lut->setSize(LUT_SIZE, LUT_SIZE, LUT_SIZE);
m_lut->setMipLevelRange(0, 0);
m_lut->setFormat(QOpenGLTexture::TextureFormat::RGBA16F);
m_lut->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
m_lut->allocateStorage();
m_lut->bind(2);
QImage colormap = loadColormap();
m_colormap = std::make_unique<QOpenGLTexture>(QOpenGLTexture::Target2DArray);
m_colormap->setSize(colormap.width());
m_colormap->setLayers(colormap.height());
m_colormap->setFormat(QOpenGLTexture::RGBA8_UNorm);
m_colormap->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
m_colormap->setWrapMode(QOpenGLTexture::ClampToEdge);
m_colormap->allocateStorage();
for(int i=0; i<colormap.height(); i++)
m_colormap->setData(0, i, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, colormap.scanLine(i));
m_colormap->bind(3);
if(m_rawImage)
setImage(m_rawImage, m_currentImg);
}
void ImageWidget::dragEnterEvent(QDragEnterEvent *event)
void ImageWidgetGL::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
event->acceptProposedAction();
}
void ImageWidget::dropEvent(QDropEvent *event)
void ImageWidgetGL::dropEvent(QDropEvent *event)
{
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
{
@@ -587,7 +866,7 @@ void ImageWidget::dropEvent(QDropEvent *event)
event->ignore();
}
void ImageWidget::mousePressEvent(QMouseEvent *event)
void ImageWidgetGL::mousePressEvent(QMouseEvent *event)
{
if(m_showThumbnails && event->button() == Qt::LeftButton && event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier))
m_selecting = true;
@@ -603,7 +882,7 @@ void ImageWidget::mousePressEvent(QMouseEvent *event)
}
}
void ImageWidget::mouseMoveEvent(QMouseEvent *event)
void ImageWidgetGL::mouseMoveEvent(QMouseEvent *event)
{
if(m_selecting)
{
@@ -638,7 +917,7 @@ void ImageWidget::mouseMoveEvent(QMouseEvent *event)
}
}
void ImageWidget::mouseReleaseEvent(QMouseEvent *event)
void ImageWidgetGL::mouseReleaseEvent(QMouseEvent *event)
{
if(m_selecting)
{
@@ -669,7 +948,7 @@ void ImageWidget::mouseReleaseEvent(QMouseEvent *event)
}
}
void ImageWidget::wheelEvent(QWheelEvent *event)
void ImageWidgetGL::wheelEvent(QWheelEvent *event)
{
if(m_showThumbnails)
{
@@ -682,7 +961,7 @@ void ImageWidget::wheelEvent(QWheelEvent *event)
}
}
void ImageWidget::thumbSelect(QMouseEvent *event)
void ImageWidgetGL::thumbSelect(QMouseEvent *event)
{
QPoint p = event->pos();
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
@@ -711,11 +990,11 @@ void ImageWidget::thumbSelect(QMouseEvent *event)
}
}
void ImageWidget::debayer()
void ImageWidgetGL::debayer()
{
if(m_debayerTex > 0 || !m_superpixel || !m_bwImg || m_imgWidth < 0)return;
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight, QOpenGLFramebufferObject::NoAttachment, GL_TEXTURE_2D, GL_RGBA16);
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight, QOpenGLFramebufferObject::NoAttachment, GL_TEXTURE_2D, GL_RGBA16F);
fbo.bind();
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
@@ -729,12 +1008,12 @@ void ImageWidget::debayer()
f->glViewport(0, 0, m_width, m_height);
m_debayerTex = fbo.takeTexture();
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
f->glGenerateMipmap(GL_TEXTURE_2D);
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
f->glGenerateMipmap(GL_TEXTURE_2D);
}
void ImageWidget::updateScrollBars()
void ImageWidgetGL::updateScrollBars()
{
if(m_showThumbnails)
emit scrollBarsUpdate(0, 0, -1, m_dy, m_height, (m_thumbnailCount / (m_width / THUMB_SIZE_BORDER) + 2) * THUMB_SIZE_BORDER_Y - m_height);
@@ -742,91 +1021,22 @@ void ImageWidget::updateScrollBars()
emit scrollBarsUpdate(m_dx, m_width, m_imgWidth * m_scale - m_width, m_dy, m_height, m_imgHeight * m_scale - m_height);
}
ImageScrollAreaGL::ImageScrollAreaGL(Database *database, QWidget *parent) : QWidget(parent)
QImage ImageWidget::loadColormap()
{
QGridLayout *layout = new QGridLayout(this);
setLayout(layout);
m_imageWidget = new ImageWidget(database, this);
m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
layout->setSpacing(0);
layout->addWidget(m_imageWidget, 0, 0);
layout->addWidget(m_verticalScrollBar, 0, 1);
layout->addWidget(m_horizontalScrollBar, 1, 0);
connect(m_verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
connect(m_horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
connect(m_imageWidget, &ImageWidget::scrollBarsUpdate, this, &ImageScrollAreaGL::updateScrollbars);
}
ImageScrollAreaGL::~ImageScrollAreaGL()
{
}
void ImageScrollAreaGL::setImage(Image *image)
{
if(image && image->rawImage())
QImage embedded(":/colormap.png");
QStringList path = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
if(path.size())
{
m_imageWidget->setImage(image->rawImage(), image->number());
m_imageWidget->setWCS(image->info().wcs);
}
}
ImageWidget *ImageScrollAreaGL::imageWidget()
{
return m_imageWidget;
}
void ImageScrollAreaGL::updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV)
{
if(maxH > 0)
QImage user(path.first() + "/colormap.png");
if(!user.isNull())
{
m_horizontalScrollBar->show();
m_horizontalScrollBar->setRange(0, maxH);
m_horizontalScrollBar->setPageStep(stepH);
m_horizontalScrollBar->setValue(valueH);
user = user.scaledToWidth(embedded.width(), Qt::SmoothTransformation);
QImage tmp(embedded.width(), embedded.height() + user.height(), QImage::Format_RGBA8888);
QPainter painter(&tmp);
painter.drawImage(0, 0, embedded);
painter.drawImage(0, embedded.height(), user);
return tmp;
}
else
m_horizontalScrollBar->hide();
if(maxV > 0)
{
m_verticalScrollBar->show();
m_verticalScrollBar->setRange(0, maxV);
m_verticalScrollBar->setPageStep(stepV);
m_verticalScrollBar->setValue(valueV);
}
else
m_verticalScrollBar->hide();
}
void ImageScrollAreaGL::zoomIn()
{
m_imageWidget->zoom(1);
}
void ImageScrollAreaGL::zoomOut()
{
m_imageWidget->zoom(-1);
}
void ImageScrollAreaGL::bestFit()
{
m_horizontalScrollBar->hide();
m_verticalScrollBar->hide();
m_imageWidget->bestFit();
}
void ImageScrollAreaGL::oneToOne()
{
m_imageWidget->zoom(0);
}
void ImageScrollAreaGL::scrollEvent()
{
m_imageWidget->setOffset(m_horizontalScrollBar->value(), m_verticalScrollBar->value());
return embedded.convertToFormat(QImage::Format_RGBA8888);
}
+63 -55
View File
@@ -1,21 +1,48 @@
#ifndef IMAGESCROLLAREAGL_H
#define IMAGESCROLLAREAGL_H
#ifndef IMAGEWIDGET_H
#define IMAGEWIDGET_H
#include <memory>
#include <QObject>
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLTexture>
#include <QOpenGLVertexArrayObject>
#include <QScrollBar>
#include <QTimer>
#include "rawimage.h"
#include "imageringlist.h"
#include <QOpenGLFunctions>
#include <QPainterPath>
#include "database.h"
#include "rawimage.h"
#include "imageinfodata.h"
#include "stretchtoolbar.h"
class ImageWidget
{
public:
ImageWidget(){}
virtual ~ImageWidget(){}
virtual void setImage(std::shared_ptr<RawImage> image, int index) = 0;
virtual void setWCS(std::shared_ptr<WCSDataT> wcs) = 0;
virtual void zoom(int zoom, const QPointF &mousePos = QPointF()) = 0;
virtual void bestFit() = 0;
virtual void setBayerMask(int mask) = 0;
virtual void setColormap(int colormap) = 0;
virtual void setOffset(float dx, float dy) = 0;
virtual void allocateThumbnails(const QStringList &paths) = 0;
virtual void setMTFParams(const MTFParam &params) = 0;
virtual void superPixel(bool enable) = 0;
virtual void invert(bool enable) = 0;
virtual void falseColor(bool enable) = 0;
virtual QImage renderToImage() = 0;
virtual void thumbnailLoaded(const Image *image) = 0;
virtual void showThumbnail(bool enable) = 0;
virtual void drawGrid(bool enable) = 0;
static QImage loadColormap();
};
struct ImageThumb
{
QString name;
@@ -25,11 +52,11 @@ struct ImageThumb
bool dirty;
};
class ImageWidget : public QOpenGLWidget
class ImageWidgetGL : public QOpenGLWidget, public ImageWidget
{
Q_OBJECT
QOpenGLFunctions *f = nullptr;
QOpenGLFunctions_3_3_Core *f3 = nullptr;
QOpenGLExtraFunctions *fx = nullptr;
QTimer *m_updateTimer = nullptr;
std::unique_ptr<QOpenGLShaderProgram> m_program;
std::unique_ptr<QOpenGLShaderProgram> m_thumbnailProgram;
@@ -39,11 +66,13 @@ class ImageWidget : public QOpenGLWidget
std::unique_ptr<QOpenGLTexture> m_image;
std::unique_ptr<QOpenGLVertexArrayObject> m_vao;
std::unique_ptr<QOpenGLVertexArrayObject> m_vaoThumb;
std::unique_ptr<QOpenGLPixelTransferOptions> m_transferOptions;
std::unique_ptr<QOpenGLTexture> m_thumbnailTexture;
std::unique_ptr<QOpenGLTexture> m_lut;
std::unique_ptr<QOpenGLTexture> m_colormap;
GLuint m_debayerTex = 0;
std::shared_ptr<RawImage> m_rawImage;
std::shared_ptr<WCSData> m_wcs;
std::shared_ptr<WCSDataT> m_wcs;
SkyGrid m_grid;
int m_width, m_height;
int m_imgWidth = -1, m_imgHeight = -1;
int m_currentImg = 0;
@@ -53,7 +82,6 @@ class ImageWidget : public QOpenGLWidget
float m_scale = 1.0f;
int m_scaleStop = 0;
bool m_bestFit = false;
bool m_blockRepaint = false;
bool m_bwImg = false;
bool m_falseColor = false;
bool m_invert = false;
@@ -62,35 +90,37 @@ class ImageWidget : public QOpenGLWidget
bool m_selecting = false;
bool m_sizesDirty = false;
bool m_srgb = false;
bool m_drawGrid = false;
int m_thumbnailCount = 0;
int m_maxTextureSize = 0;
int m_maxArrayLayers = 0;
int m_firstRed[2] = {0, 0};
int m_colormapIdx = 0;
QVector<ImageThumb> m_thumnails;
Database *m_database = nullptr;
QPointF m_lastPos;
QString m_error;
bool m_swPaint = false;
public:
explicit ImageWidget(Database *database, QWidget *parent = nullptr);
~ImageWidget() override;
void setImage(std::shared_ptr<RawImage> image, int index);
void setImage(const QPixmap &pixmap);
void setWCS(std::shared_ptr<WCSData> wcs);
void zoom(int zoom, const QPointF &mousePos = QPointF());
void bestFit();
void blockRepaint(bool block);
void allocateThumbnails(const QStringList &paths);
explicit ImageWidgetGL(Database *database, QWidget *parent = nullptr);
~ImageWidgetGL() override;
void setImage(std::shared_ptr<RawImage> image, int index) override;
void setWCS(std::shared_ptr<WCSDataT> wcs) override;
void zoom(int zoom, const QPointF &mousePos = QPointF()) override;
void bestFit() override;
void allocateThumbnails(const QStringList &paths) override;
QVector2D getImagePixelCoord(const QVector2D &pos);
void setBayerMask(int mask);
public slots:
void setMTFParams(const MTFParam &params);
void setOffset(float dx, float dy);
void superPixel(bool enable);
void invert(bool enable);
void falseColor(bool enable);
QImage renderToImage();
void thumbnailLoaded(const Image *image);
void showThumbnail(bool enable);
void setBayerMask(int mask) override;
void setColormap(int colormap) override;
void setOffset(float dx, float dy) override;
void setMTFParams(const MTFParam &params) override;
void superPixel(bool enable) override;
void invert(bool enable) override;
void falseColor(bool enable) override;
QImage renderToImage() override;
void thumbnailLoaded(const Image *image) override;
void showThumbnail(bool enable) override;
void drawGrid(bool enable) override;
protected:
void paintGL() override;
void resizeGL(int w, int h) override;
@@ -110,26 +140,4 @@ signals:
void scrollBarsUpdate(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
};
class ImageScrollAreaGL : public QWidget
{
Q_OBJECT
QScrollBar *m_verticalScrollBar;
QScrollBar *m_horizontalScrollBar;
ImageWidget *m_imageWidget;
public:
explicit ImageScrollAreaGL(Database *database, QWidget *parent = nullptr);
~ImageScrollAreaGL() override;
void setImage(Image *image);
ImageWidget* imageWidget();
protected:
void updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
public slots:
void zoomIn();
void zoomOut();
void bestFit();
void oneToOne();
protected slots:
void scrollEvent();
};
#endif // IMAGESCROLLAREAGL_H
#endif // IMAGEWIDGET_H
+498
View File
@@ -0,0 +1,498 @@
#include "loadimage.h"
#include <QElapsedTimer>
#include <QDebug>
#include <QFileInfo>
#include <QDir>
#include <libraw/libraw.h>
#include <fitsio2.h>
#include "libxisf.h"
#include <libexif/exif-data.h>
#include "rawimage.h"
QString makeUNCPath(const QString &path)
{
#ifdef Q_OS_WIN64
if(!path.startsWith("\\\\") && !path.startsWith("//"))
{
QString tmp;
QFileInfo info(path);
tmp = info.absoluteFilePath();
tmp = QDir::toNativeSeparators(tmp);
tmp.prepend("\\\\?\\");
qDebug() << "makeMaxPath" << path << tmp;
return tmp;
}
#endif
return path;
}
int loadFITSHeader(fitsfile *file, ImageInfoData &info)
{
int imgtype;
int naxis;
long naxes[3] = {0};
int nexist;
int status = 0;
char key[FLEN_KEYWORD];
char val[FLEN_VALUE];
char comm[FLEN_COMMENT];
char strval[FLEN_VALUE];
QVariant var;
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
fits_get_hdrspace(file, &nexist, nullptr, &status);
for(int i=1; i<=nexist; i++)
{
fits_read_keyn(file, i, key, val, comm, &status);
fits_read_key(file, TSTRING, key, strval, nullptr, &status);
if(status == 0 || status == VALUE_UNDEFINED)
{
QString string(strval);
bool isint;
bool isdouble;
double vald = string.toDouble(&isdouble);
long long vall = string.toLongLong(&isint);
if(isint)
var = vall;
else if(isdouble)
var = vald;
else if(status == VALUE_UNDEFINED)
var = QVariant();
else if(string == "T" || string == "F")
var = string == "T";
else
var = string;
status = 0;
info.fitsHeader.append(FITSRecord(key, var, comm));
}
else
{
return status;
}
}
char *header = nullptr;
int nrec = 0;
const char *exclist[] = {"PV1_1", "PV1_2"};
fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
if(status == 0)
{
info.wcs = std::make_shared<WCSDataT>(naxes[0], naxes[1], header, nrec);
if(!info.wcs->valid())info.wcs.reset();
}
fits_free_memory(header, &status);
return status;
}
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
{
fitsfile *file;
int status = 0;
int num = 0;
long naxes[3] = {0};
auto checkError = [&info, &status]()
{
char err[100];
fits_get_errstatus(status, err);
info.info.append({QObject::tr("Error"), QString(err)});
qWarning() << "Failed to load FITS file" << err;
return false;
};
fits_open_diskfile(&file, path.toLocal8Bit().data(), READONLY, &status);
if(status)return checkError();
fits_get_num_hdus(file, &num, &status);
if(status)return checkError();
int hdutype;
int imgtype;
int naxis;
std::vector<int> imageIdxs;
for(int i = 1; i <= num; i++)
{
fits_movabs_hdu(file, i, &hdutype, &status);if(status)return checkError();
if(hdutype == IMAGE_HDU)
{
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
}
}
info.num = imageIdxs.size();
info.index = index;
if(index >= imageIdxs.size())return false;
fits_movabs_hdu(file, imageIdxs[index], &hdutype, &status);if(status)return checkError();
if(hdutype == IMAGE_HDU)
{
naxes[0] = naxes[1] = naxes[2] = 0;
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();
if(hdutype == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
{
RawImage::DataType type;
int fitstype;
long fpixel[3] = {1,1,1};
switch(imgtype)
{
case BYTE_IMG:
type = RawImage::UINT8;
fitstype = TBYTE;
break;
case SHORT_IMG:
type = RawImage::UINT16;
fitstype = TSHORT;
break;
case USHORT_IMG:
type = RawImage::UINT16;
fitstype = TUSHORT;
break;
case LONG_IMG:
type = RawImage::UINT32;
fitstype = TINT;
break;
case ULONG_IMG:
type = RawImage::UINT32;
fitstype = TUINT;
break;
case FLOAT_IMG:
type = RawImage::FLOAT32;
fitstype = TFLOAT;
break;
case DOUBLE_IMG:
type = RawImage::FLOAT64;
fitstype = TDOUBLE;
break;
default:
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
goto noload;
break;
}
size_t size = naxes[0]*naxes[1];
size_t w = naxes[0];
size_t h = naxes[1];
info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
info.info.append({QObject::tr("Height"), QString::number(naxes[1])});
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
uint8_t *data = static_cast<uint8_t*>(img.data());
for (int i=1; i==1 || i<=naxes[2]; i++)
{
fpixel[2] = i;
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
if(status)return checkError();
}
if(fitstype == TSHORT)
{
uint16_t *s = static_cast<uint16_t*>(img.data());
size_t size = img.size() * img.channels();
for(size_t i=0; i<size; i++)
s[i] -= INT16_MIN;
}
else if(fitstype == TINT)
{
uint32_t *s = static_cast<uint32_t*>(img.data());
size_t size = img.size() * img.channels();
for(size_t i=0; i<size; i++)
s[i] -= INT32_MIN;
}
if(img.channels() == 1 || planar)
image = std::make_shared<RawImage>(std::move(img));
else
image = RawImage::fromPlanar(img);
}
}
noload:
if(file)
{
status = loadFITSHeader(file, info);
if(status)return checkError();
}
if(image)
{
for(auto fits : info.fitsHeader)
{
if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP")
image->flip();
}
}
fits_close_file(file, &status);
return true;
}
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
{
try
{
LibXISF::XISFReader xisf;
xisf.open(path.toLocal8Bit().data());
if(index >= (uint32_t)xisf.imagesCount())return false;
const LibXISF::Image &xisfImage = xisf.getImage(index);
auto fitskeywords = xisfImage.fitsKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append(fits);
}
QVector<FITSRecord> xisfWCS;
auto imageproperties = xisfImage.imageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
if(prop.id == "PCL:AstrometricSolution:ReferenceCelestialCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
{
auto val = prop.value.value<LibXISF::F64Vector>();
if(val.size() >= 2)
{
xisfWCS.append({"CRVAL1", val[0], "value from PCL:AstrometricSolution"});
xisfWCS.append({"CRVAL2", val[1], "value from PCL:AstrometricSolution"});
}
}
else if(prop.id == "PCL:AstrometricSolution:ReferenceImageCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
{
auto val = prop.value.value<LibXISF::F64Vector>();
if(val.size() >= 2)
{
xisfWCS.append({"CRPIX1", val[0], "value from PCL:AstrometricSolution"});
xisfWCS.append({"CRPIX2", val[1], "value from PCL:AstrometricSolution"});
}
}
else if(prop.id == "PCL:AstrometricSolution:LinearTransformationMatrix" && prop.value.type() == LibXISF::Variant::Type::F64Matrix)
{
auto val = prop.value.value<LibXISF::F64Matrix>();
if(val.cols() >= 2 && val.rows() >= 2)
{
xisfWCS.append({"CD1_1", val(0, 0), "value from PCL:AstrometricSolution"});
xisfWCS.append({"CD1_2", val(0, 1), "value from PCL:AstrometricSolution"});
xisfWCS.append({"CD2_1", val(1, 0), "value from PCL:AstrometricSolution"});
xisfWCS.append({"CD2_2", val(1, 1), "value from PCL:AstrometricSolution"});
}
}
else if(prop.id == "PCL:AstrometricSolution:ProjectionSystem")
{
if(prop.value.toString() == "Gnomonic")
{
xisfWCS.append({"CTYPE1", "RA---TAN", "value from PCL:AstrometricSolution"});
xisfWCS.append({"CTYPE", "DEC--TAN", "value from PCL:AstrometricSolution"});
}
}
}
info.num = xisf.imagesCount();
info.index = index;
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
auto wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
if(!wcs->valid() && xisfWCS.size())wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), xisfWCS);
if(wcs->valid())info.wcs = wcs;
RawImage::DataType type;
switch(xisfImage.sampleFormat())
{
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
default: break;
}
LibXISF::Image tmpImage = xisfImage;
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
{
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
image->setICCProfile(tmpImage.iccProfile());
return true;
}
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
{
if(planar)
{
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize());
}
else
{
image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
}
image->setICCProfile(tmpImage.iccProfile());
return true;
}
return false;
}
catch (LibXISF::Error &err)
{
info.info.append(QPair<QString, QString>("Error", err.what()));
qWarning() << "Failed to load XISF" << err.what();
return false;
}
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
return false;
}
bool readFITSHeader(const QString &path, ImageInfoData &info)
{
fitsfile *fr;
int status = 0;
QString path2 = makeUNCPath(path);
fits_open_diskfile(&fr, path2.toLocal8Bit().data(), READONLY, &status);
if(fr && status == 0)
{
status = loadFITSHeader(fr, info);
fits_close_file(fr, &status);
}
return status == 0;
}
bool readXISFHeader(const QString &path, ImageInfoData &info)
{
QString path2 = makeUNCPath(path);
try
{
LibXISF::XISFReader xisf;
xisf.open(path2.toLocal8Bit().data());
const LibXISF::Image &image = xisf.getImage(0, false);
auto fitskeywords = image.fitsKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append(fits);
}
auto imageproperties = image.imageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
}
info.wcs = std::make_shared<WCSDataT>(image.width(), image.height(), info.fitsHeader);
if(!info.wcs->valid())info.wcs.reset();
}
catch (LibXISF::Error &err)
{
qWarning() << "LibXISF error" << err.what();
return false;
}
return true;
}
void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag)
{
char val[1024];
ExifEntry *entry = exif_content_get_entry(content, tag);
if(entry)
{
exif_entry_get_value(entry, val, sizeof(val));
info.info.append({exif_tag_get_title(tag), val});
}
}
bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
{
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
raw->open_file(path.toLocal8Bit().data());
raw->imgdata.params.half_size = true;
raw->imgdata.params.use_camera_wb = true;
raw->imgdata.params.user_flip = 0;
if(raw->unpack())
return false;
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
size_t size = rawdata.sizes.width*rawdata.sizes.height;
std::vector<uint16_t> out;
out.resize(size);
size_t d = 0;
uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);
for(size_t i=rawdata.sizes.top_margin;i<h;i++)
{
for(size_t o=rawdata.sizes.left_margin;o<w;o++)
{
uint16_t p = rawdata.raw_image[i*pitch+o];
out[d++] = p;
}
}
image = std::make_shared<RawImage>(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16);
memcpy(image->data(), &out[0], sizeof(uint16_t)*d);
QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
if(raw->imgdata.other.shutter < 1)
{
shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
}
info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)});
info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)});
info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
#if LIBRAW_MINOR_VERSION>=19
// info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
#endif
return true;
}
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar)
{
bool ret = false;
QElapsedTimer timer;
QFileInfo fileInfo(path);
timer.start();
if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
{
ret = loadRAW(path, info, rawImage);
qDebug() << "LoadRAW" << timer.elapsed() << "ms";
}
else if(isFITS(fileInfo.suffix()))
{
ret = loadFITS(path, info, rawImage, planar, index);
qDebug() << "LoadFITS" << timer.elapsed() << "ms";
}
else if(isXISF(fileInfo.suffix()))
{
ret = loadXISF(path, info, rawImage, planar, index);
qDebug() << "LoadXISF" << timer.elapsed() << "ms";
}
else
{
QImage img(path);
ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData());
info.info.append({QObject::tr("Width"), QString::number(img.width())});
info.info.append({QObject::tr("Height"), QString::number(img.height())});
if(exif)
{
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
exif_data_free(exif);
}
rawImage = std::make_shared<RawImage>(img);
qDebug() << "LoadQImage" << timer.elapsed() << "ms";
ret = !img.isNull();
}
return ret;
}
bool isFITS(const QString &suffix)
{
return suffix.compare("fits", Qt::CaseInsensitive) == 0 || suffix.compare("fit", Qt::CaseInsensitive) == 0 || suffix.compare("fts", Qt::CaseInsensitive) == 0 || suffix.compare("fz", Qt::CaseInsensitive) == 0;
}
bool isXISF(const QString &suffix)
{
return suffix.compare("xisf", Qt::CaseInsensitive) == 0;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef LOADIMAGE_H
#define LOADIMAGE_H
#include <QString>
#include "imageinfodata.h"
class RawImage;
QString makeUNCPath(const QString &path);
bool readFITSHeader(const QString &path, ImageInfoData &info);
bool readXISFHeader(const QString &path, ImageInfoData &info);
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar = false);
bool isFITS(const QString &suffix);
bool isXISF(const QString &suffix);
#endif // LOADIMAGE_H
+374
View File
@@ -0,0 +1,374 @@
#include "loadrunable.h"
#include "imageringlist.h"
#include <QFileInfo>
#include <QPainter>
#include <QElapsedTimer>
#include <QDebug>
#include <algorithm>
#include <fitsio2.h>
#include "rawimage.h"
#include "loadimage.h"
#include <lcms2.h>
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail) :
m_file(makeUNCPath(file)),
m_receiver(receiver),
m_analyzeLevel(level),
m_thumbnail(thumbnail),
m_index(index)
{
}
void LoadRunable::run()
{
try
{
if(!m_thumbnail && !m_receiver->isCurrent())
{
return;
}
QElapsedTimer timer;
ImageInfoData info;
QFileInfo finfo(m_file);
info.info.append({QObject::tr("Filename"), finfo.fileName()});
std::shared_ptr<RawImage> rawImage;
if(!loadImage(m_file, info, rawImage, m_index))
info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")});
if(rawImage && !m_thumbnail)
{
rawImage->convertToGLFormat();
timer.start();
rawImage->generateLUT();
qDebug() << "generate LUT" << timer.restart();
//rawImage->convertTosRGB();
//qDebug() << "convert" << timer.restart();
rawImage->calcStats();
const RawImage::Stats &stats = rawImage->imageStats();
qDebug() << "image stats" << timer.restart();
if(rawImage->channels() == 1)
{
info.info.append({QObject::tr("Mean"), QString::number(stats.m_mean[0])});
info.info.append({QObject::tr("Standart deviation"), QString::number(stats.m_stdDev[0])});
info.info.append({QObject::tr("Median"), QString::number(stats.m_median[0])});
info.info.append({QObject::tr("Minimum"), QString::number(stats.m_min[0])});
info.info.append({QObject::tr("Maximum"), QString::number(stats.m_max[0])});
info.info.append({QObject::tr("MAD"), QString::number(stats.m_mad[0])});
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * stats.m_saturated[0] / rawImage->size()) + "%"});
}
else
{
info.info.append({QObject::tr("Mean"), QString("%1 %2 %3").arg(stats.m_mean[0]).arg(stats.m_mean[1]).arg(stats.m_mean[2])});
info.info.append({QObject::tr("Standart deviation"), QString("%1 %2 %3").arg(stats.m_stdDev[0]).arg(stats.m_stdDev[1]).arg(stats.m_stdDev[2])});
info.info.append({QObject::tr("Median"), QString("%1 %2 %3").arg(stats.m_median[0]).arg(stats.m_median[1]).arg(stats.m_median[2])});
info.info.append({QObject::tr("Minimum"), QString("%1 %2 %3").arg(stats.m_min[0]).arg(stats.m_min[1]).arg(stats.m_min[2])});
info.info.append({QObject::tr("Maximum"), QString("%1 %2 %3").arg(stats.m_max[0]).arg(stats.m_max[1]).arg(stats.m_max[2])});
info.info.append({QObject::tr("MAD"), QString("%1 %2 %3").arg(stats.m_mad[0]).arg(stats.m_mad[1]).arg(stats.m_mad[2])});
info.info.append({QObject::tr("Saturated"), QString("%1 %2 %3%").arg(100.0 * stats.m_saturated[0] / rawImage->size())
.arg(100.0 * stats.m_saturated[1] / rawImage->size())
.arg(100.0 * stats.m_saturated[2] / rawImage->size())});
}
}
if(m_thumbnail)
{
if(rawImage && rawImage->valid())
{
if(QUALITY_RESIZE)
rawImage->resize(THUMB_SIZE, THUMB_SIZE);
rawImage->convertToGLFormat();
rawImage->convertToThumbnail();
}
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
}
else
{
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, info));
}
}
catch(std::exception e)
{
qDebug() << m_file << e.what();
std::shared_ptr<RawImage> rawImage;
if(m_thumbnail)
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
else
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, ImageInfoData()));
}
}
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams &params, QSemaphore *semaphore) :
m_infile(makeUNCPath(in)),
m_outfile(makeUNCPath(out)),
m_format(format),
m_params(params),
m_semaphore(semaphore)
{
}
void writeFITSImage(fitsfile *fw, std::shared_ptr<RawImage> rawimage, ImageInfoData &imageinfo)
{
static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"};
int status = 0;
long firstpix[3] = {1,1,1};
int channels = rawimage->channels();
int naxis = channels == 1 ? 2 : 3;
long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->channels()};
std::vector<RawImage> planes;
if(channels == 1)
planes.push_back(*rawimage);
else
planes = rawimage->split();
switch(rawimage->type())
{
case RawImage::UINT8:
fits_create_img(fw, BYTE_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TBYTE, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
case RawImage::UINT16:
fits_create_img(fw, USHORT_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TUSHORT, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
case RawImage::FLOAT32:
fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status);
for(int i=0; i<channels; i++)
{
firstpix[2] = i+1;
fits_write_pix(fw, TFLOAT, firstpix, rawimage->size(), planes[i].data(), &status);
}
break;
default:
return;
}
for(const FITSRecord &record : imageinfo.fitsHeader)
{
if(skipKeys.contains(record.key) || record.xisf)continue;
bool isdouble;
bool isint;
bool isbool = record.value.toString() == "T" || record.value.toString() == "F";
double vald = record.value.toDouble(&isdouble);
int valb = record.value.toString() == "T";
long long vall = record.value.toLongLong(&isint);
if(isint)isint = vall == vald;
QByteArray str = record.value.toString().toLatin1();
if(isint)
fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(isdouble)
fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(isbool)
fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(record.key == "COMMENT")
fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else if(record.key == "HISTORY")
fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
else
fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
}
}
void ConvertRunable::run()
{
QSemaphoreReleaser release;
if(m_semaphore)release = QSemaphoreReleaser(m_semaphore);
ImageInfoData imageinfo;
std::shared_ptr<RawImage> rawimage;
loadImage(m_infile, imageinfo, rawimage, 0);
QFileInfo info(m_outfile);
info.dir().mkpath(".");
if(m_params.stretch)
{
rawimage->applySTF(m_params.mtf);
}
else if(m_params.autostretch)
{
rawimage->calcStats();
MTFParam mtfParam = rawimage->calcMTFParams();
rawimage->applySTF(mtfParam);
}
if(m_params.binning > 1)
{
rawimage->resizeInt(m_params.binning, m_params.average);
}
if(m_params.resize.isValid() && !m_params.resize.isEmpty())
{
QSize imgSize(rawimage->width(), rawimage->height());
imgSize = imgSize.scaled(m_params.resize, m_params.aspect);
rawimage->resize(imgSize.width(), imgSize.height());
}
if(rawimage)
{
if(m_format == "xisf")
{
try
{
LibXISF::XISFWriter xisf;
int channelCount = rawimage->channels();
LibXISF::Image::SampleFormat sampleFormat;
switch(rawimage->type())
{
case RawImage::UINT8: sampleFormat = LibXISF::Image::UInt8; break;
case RawImage::UINT16: sampleFormat = LibXISF::Image::UInt16; break;
case RawImage::FLOAT32: sampleFormat = LibXISF::Image::Float32; break;
default: return;
}
LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Planar);
if(channelCount == 1)
{
std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize());
}
else
{
size_t off = 0;
std::vector<RawImage> planes = rawimage->split();
for(const auto &plane : planes)
{
std::memcpy(image.imageData<uint8_t>() + off, plane.data(), plane.size() * RawImage::typeSize(plane.type()));
off += plane.size() * RawImage::typeSize(plane.type());
}
}
for(auto &record : imageinfo.fitsHeader)
{
if(record.xisf)continue;
if(record.value.typeId() == QMetaType::Bool)
image.addFITSKeyword({record.key.toStdString(), record.value.toBool() ? "T" : "F", record.comment.toStdString()});
else
image.addFITSKeyword({record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});
}
if(m_params.compressionType.startsWith("zstd") && LibXISF::DataBlock::CompressionCodecSupported(LibXISF::DataBlock::ZSTD))
image.setCompression(LibXISF::DataBlock::ZSTD, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("lz4hc"))
image.setCompression(LibXISF::DataBlock::LZ4HC, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("lz4"))
image.setCompression(LibXISF::DataBlock::LZ4, m_params.compressionLevel);
else if(m_params.compressionType.startsWith("zlib"))
image.setCompression(LibXISF::DataBlock::Zlib, m_params.compressionLevel);
if(m_params.compressionType.endsWith("+sh"))
image.setByteshuffling(true);
xisf.writeImage(image);
xisf.save(m_outfile.toLocal8Bit().data());
}
catch(LibXISF::Error &err)
{
qDebug() << "Failed to save XISF image" << err.what();
}
return;
}
if(m_format == "fits")
{
int status = 0;
fitsfile *fw;
if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile);
fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status);
if(!m_params.compressionType.isEmpty())
{
if(m_params.compressionType == "gzip")
fits_set_compression_type(fw, GZIP_1, &status);
else if(m_params.compressionType == "rice")
fits_set_compression_type(fw, RICE_1, &status);
}
writeFITSImage(fw, rawimage, imageinfo);
fits_close_file(fw, &status);
return;
}
// if nothing else try QImage
{
QImage::Format format = QImage::Format_Invalid;
switch(rawimage->type())
{
case RawImage::UINT8:
if(rawimage->channels() == 1)format = QImage::Format_Grayscale8;
else if(rawimage->channels() == 3)format = QImage::Format_RGBX8888;
else if(rawimage->channels() == 4)format = QImage::Format_RGBA8888;
break;
case RawImage::UINT16:
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
break;
case RawImage::FLOAT16:
case RawImage::FLOAT32:
case RawImage::FLOAT64:
case RawImage::UINT32:
rawimage->convertToType(RawImage::UINT16);
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
break;
}
if(format == QImage::Format_Invalid)return;
QImage qimage((const uchar*)rawimage->data(), rawimage->width(), rawimage->height(), rawimage->widthBytes(), format);
qimage.save(m_outfile);
}
}
}
ConvertRunable::ConvertParams::ConvertParams(const QVariantMap &map)
{
bool ok = false;
if(map.contains("compressionLevel"))
compressionLevel = std::clamp(map["compressionLevel"].toInt(&ok), -1, 100);
if(!ok)compressionLevel = -1;
if(map.contains("compressionType"))
compressionType = map["compressionType"].toString();
if(map.contains("binning"))
binning = map["binning"].toInt();
if(map.contains("average"))
average = map["average"].toBool();
if(map.contains("resize"))
{
QVariantMap size = map["resize"].toMap();
if(size.contains("width") && size.contains("height"))
{
int w = size["width"].toInt();
int h = size["height"].toInt();
resize = QSize(w, h);
}
if(size.contains("aspect"))
{
QString aspectStr = map["aspect"].toString();
if(aspectStr == "keep")
aspect = Qt::KeepAspectRatio;
else if(aspectStr == "expand")
aspect = Qt::KeepAspectRatioByExpanding;
else if(aspectStr == "ignore")
aspect = Qt::IgnoreAspectRatio;
}
}
if(map.contains("autostretch"))
autostretch = map["autostretch"].toBool();
}
+12 -9
View File
@@ -4,13 +4,9 @@
#include <QRunnable>
#include <QString>
#include <QSemaphore>
#include "imageinfo.h"
class RawImage;
bool readFITSHeader(const QString &path, ImageInfoData &info);
bool readXISFHeader(const QString &path, ImageInfoData &info);
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage);
#include <QSize>
#include "imageinfodata.h"
#include "mtfparam.h"
class Image;
@@ -20,12 +16,12 @@ class LoadRunable : public QRunnable
Image *m_receiver;
AnalyzeLevel m_analyzeLevel;
bool m_thumbnail;
int m_index = 0;
public:
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail = false);
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail = false);
void run() override;
};
class ConvertRunable : public QRunnable
{
public:
@@ -33,6 +29,13 @@ public:
{
int compressionLevel = -1;
QString compressionType;
int binning = 0;
bool average = true;
QSize resize;
Qt::AspectRatioMode aspect = Qt::KeepAspectRatio;
bool autostretch = false;
bool stretch = false;
MTFParam mtf;
ConvertParams(){}
ConvertParams(const QVariantMap &map);
};
+163
View File
@@ -0,0 +1,163 @@
#include "mainwindow.h"
#include <QApplication>
#include <QCommandLineParser>
#include <QSettings>
#include <QSurfaceFormat>
#include <QTranslator>
#include <stdlib.h>
#include "../thumbnailer/genthumbnail.h"
#ifdef Q_OS_WIN64
#include <windows.h>
#endif
bool DEBUG_LOG = false;
QtMessageHandler defaultHandler = nullptr;
void messageHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
{
if(defaultHandler)
{
if(DEBUG_LOG || type != QtMsgType::QtDebugMsg)
defaultHandler(type, ctx, message);
}
}
int main(int argc, char *argv[])
{
#ifdef __linux__
setenv("LC_NUMERIC", "C", 1);
#endif
#if defined(__i386__) || defined(__x86_64__) || defined(__APPLE__)
bool useGLES = false;
#else
bool useGLES = true;
#endif
#ifdef Q_OS_WIN64
if(AttachConsole(ATTACH_PARENT_PROCESS))
{
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
defaultHandler = qInstallMessageHandler(messageHandler);
QCommandLineParser cmd;
cmd.addOption({"gl", "Use desktop OpenGL. This is default on x86 and MacOS platform."});
cmd.addOption({"gles", "Use OpenGL ES. This is default on ARM platform."});
cmd.addOption({{"thumb", "thumbnail"}, "Generate thumbnail and save it to path.", "path"});
cmd.addOption({{"s", "size"}, "Size of the thumbnails in pixels (default: 128)", "size", "128"});
cmd.addPositionalArgument("file", "Files or paths to open");
cmd.addOption({"script", "Execute script", "script"});
cmd.addOption({"scriptarg", "String that will be passed to script as variable \"scriparg\"", "arg"});
cmd.addOption({"outdir", "Output dir for script (default: CWD)", "dir", "."});
cmd.addOption({"noexit", "Do not exit application when script finish"});
cmd.addOption({"debug", "Print debug info"});
cmd.addHelpOption();
QStringList cmdArgs;
for(int i = 0; i < argc; i++)
cmdArgs.append(argv[i]);
cmd.process(cmdArgs);
if(cmd.isSet("gl"))
useGLES = false;
if(cmd.isSet("gles"))
useGLES = true;
if(cmd.isSet("debug"))
DEBUG_LOG = true;
if(cmd.isSet("thumb"))
{
QCoreApplication app(argc, argv);
QStringList files = cmd.positionalArguments();
if(files.size() == 0)
return 1;
QString thumb = cmd.value("thumb");
int size = 128;
bool ok;
int size2 = cmd.value("s").toInt(&ok);
if(ok)
size = size2;
return generateThumbnail(files.front(), thumb, size);
}
QSurfaceFormat format;
if(useGLES)
{
format.setMajorVersion(3);
format.setMinorVersion(0);
format.setRenderableType(QSurfaceFormat::OpenGLES);
}
else
{
format.setMajorVersion(3);
format.setMinorVersion(3);
//format.setOption(QSurfaceFormat::DebugContext);
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
}
QSurfaceFormat::setDefaultFormat(format);
QApplication a(argc, argv);
a.setOrganizationName("nou");
a.setApplicationName("Tenmon");
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
QTranslator translator;
QTranslator translator2;
QSettings settings;
QString lang = settings.value("settings/lang").toString();
if(lang.isEmpty())
{
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
a.installTranslator(&translator);
}
else
{
if(translator.load("tenmon_" + lang, ":/translations"))
a.installTranslator(&translator);
}
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
a.installTranslator(&translator2);
MainWindow w;
w.show();
if(!cmd.positionalArguments().isEmpty() && !cmd.isSet("script"))
{
QStringList files = cmd.positionalArguments();
QStringList paths;
for(auto &arg : files)
{
QUrl url(arg);
QFileInfo info(url.isLocalFile() ? url.toLocalFile() : arg);
if(info.exists())
paths.append(info.canonicalFilePath());
}
if(paths.size() == 1)
w.loadFile(paths.front());
else if(paths.size() > 1)
w.loadFiles(paths);
}
if(cmd.isSet("script"))
{
QStringList paths = cmd.positionalArguments();
QString script = cmd.value("script");
QString outdir = cmd.value("outdir");
QString arg = cmd.value("scriptarg");
if(!QDir::isAbsolutePath(script))script = QDir::currentPath() + "/" + script;
if(!QDir::isAbsolutePath(outdir))outdir = QDir::currentPath() + "/" + outdir;
bool noexit = cmd.isSet("noexit");
if(!noexit)w.hide();
w.runScript(script, outdir, paths, arg, !noexit);
}
return a.exec();
}
+312 -128
View File
@@ -18,7 +18,12 @@
#include <QThreadPool>
#include <QStatusBar>
#include <QImageReader>
#include <QImageWriter>
#include <QMimeDatabase>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QTimer>
#include "loadrunable.h"
#include "markedfiles.h"
#include "about.h"
@@ -26,6 +31,7 @@
#include "settingsdialog.h"
#include "histogram.h"
#include "batchprocessing.h"
#include "filemanager.h"
#ifdef __linux__
#include <sys/ioctl.h>
@@ -53,58 +59,92 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
for(auto format : supportedFormats)
{
QMimeType mimeType = db.mimeTypeForName(format);
_saveFilter.append(mimeType.filterString() + ";;");
_openFilter.append("*.");
_openFilter.append(mimeType.suffixes().join(" *."));
_openFilter.append(" ");
nameFilter.append(mimeType.suffixes());
}
_openFilter.append("*.fit *.fits *.xisf *.cr2 *.cr3 *.nef *.dng)");
auto supportedWrite = QImageWriter::supportedMimeTypes();
for(auto format : supportedWrite)
{
QMimeType mimeType = db.mimeTypeForName(format);
_saveFilter.append(mimeType.filterString() + ";;");
}
_openFilter.append("*.fit *.fits *.fts *.fz *.xisf *.cr2 *.cr3 *.nef *.dng)");
_openFilter.append(tr(";;All files (*)"));
nameFilter.append({"fit", "fits", "xisf", "cr2", "cr3", "nef", "dng"});
nameFilter.append({"fit", "fits", "fts", "fz", "xisf", "cr2", "cr3", "nef", "dng"});
QImageReader::setAllocationLimit(0);
_openSuffix = {nameFilter.constBegin(), nameFilter.constEnd()};
m_info = new ImageInfo(this);
QDockWidget *infoDock = new QDockWidget(tr("Image info"), this);
infoDock->setWidget(m_info);
infoDock->setObjectName("infoDock");
addDockWidget(Qt::LeftDockWidgetArea, infoDock);
resize(800, 600);
resize(1024, 600);
setStatusBar(new QStatusBar(this));
m_database = new Database(this);
if(!m_database->init())
QMessageBox::critical(this, tr("Can't open DB"), tr("Can't open SQLITE database"));
m_imageGL = new ImageScrollAreaGL(m_database, this);
setCentralWidget(m_imageGL);
m_image = new ImageScrollArea(m_database, this);
setCentralWidget(m_image);
StatusBar *statusBar = new StatusBar(this);
setStatusBar(statusBar);
connect(m_imageGL->imageWidget(), &ImageWidget::status, statusBar, &StatusBar::newStatus);
connect(m_image, &ImageScrollArea::status, statusBar, &StatusBar::newStatus);
m_stretchPanel = new StretchToolbar(this);
connect(m_stretchPanel, &StretchToolbar::paramChanged, m_imageGL->imageWidget(), &ImageWidget::setMTFParams);
connect(m_stretchPanel, &StretchToolbar::paramChanged, m_image, &ImageScrollArea::setMTFParams);
connect(m_stretchPanel, &StretchToolbar::autoStretch, [&](){ m_stretchPanel->stretchImage(m_ringList->currentImage().get()); });
connect(m_stretchPanel, &StretchToolbar::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
connect(m_stretchPanel, &StretchToolbar::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
connect(m_stretchPanel, &StretchToolbar::falseColor, m_imageGL->imageWidget(), &ImageWidget::falseColor);
connect(m_stretchPanel, &StretchToolbar::invert, m_image, &ImageScrollArea::invert);
connect(m_stretchPanel, &StretchToolbar::superPixel, m_image, &ImageScrollArea::superPixel);
connect(m_stretchPanel, &StretchToolbar::falseColor, m_image, &ImageScrollArea::falseColor);
connect(m_stretchPanel, &StretchToolbar::drawGrid, m_image, &ImageScrollArea::drawGrid);
m_ringList = new ImageRingList(m_database, nameFilter, this);
m_filesystem = new FilesystemWidget(m_ringList, this);
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
connect(m_filesystem, &FilesystemWidget::fileSelected, this, static_cast<void (MainWindow::*)(int)>(&MainWindow::loadFile));
connect(m_filesystem, &FilesystemWidget::sortChanged, m_ringList, &ImageRingList::setSort);
connect(m_filesystem, &FilesystemWidget::reverseSort, m_ringList, &ImageRingList::reverseSort);
m_filetree = nullptr;
#if !defined(FLATPAK) || !defined(__aarch64__)//bug with QTreeView and QFileSystemModel on ARM64 under flatpak
m_filetree = new Filetree(this);
connect(m_filetree, &Filetree::fileSelected, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
connect(m_filetree, &Filetree::copyFiles, [this](const QString &path){ copyOrMove(true, path); });
connect(m_filetree, &Filetree::moveFiles, [this](const QString &path){ copyOrMove(false, path); });
connect(m_filetree, &Filetree::indexDirectory, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::indexDir));
#endif
m_databaseView = new DataBaseView(m_database, this);
connect(m_databaseView, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
connect(m_databaseView, &DataBaseView::loadFile, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
#ifdef PLATESOLVER
_plateSolving = new PlateSolving(this);
addDockWidget(Qt::RightDockWidgetArea, _plateSolving);
_plateSolving->hide();
#endif
_databaseTree = new DatabaseTree(m_database, this);
QToolBar *navigationToolbar = new QToolBar(tr("Navigation toolbar"), this);
navigationToolbar->setObjectName("navigationtoolbar");
navigationToolbar->hide();
QAction *prevAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowLeft), tr("Previous image"));
prevAction->setShortcuts({Qt::Key_Left, Qt::Key_Up});
QAction *nextAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("Next image"));
nextAction->setShortcuts({Qt::Key_Right, Qt::Key_Down});
QAction *prevSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowUp), tr("Prev sub image"), Qt::Key_PageUp);
QAction *nextSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowDown), tr("Next sub image"), Qt::Key_PageDown);
connect(prevAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::decrement));
connect(nextAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
connect(prevSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::prevSubImage));
connect(nextSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::nextSubImage));
addToolBar(Qt::TopToolBarArea, navigationToolbar);
addToolBar(Qt::TopToolBarArea, m_stretchPanel);
QDockWidget *filesystemDock = new QDockWidget(tr("Filesystem"), this);
@@ -116,13 +156,17 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
databaseViewDock->setWidget(m_databaseView);
databaseViewDock->setObjectName("databaseViewDock");
databaseViewDock->hide();
connect(databaseViewDock, &QDockWidget::visibilityChanged, m_databaseView, &DataBaseView::visible);
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
QDockWidget *filetreeDock = new QDockWidget(tr("File tree"), this);
QDockWidget *filetreeDock = nullptr;
#if !defined(FLATPAK) || !defined(__aarch64__)
filetreeDock = new QDockWidget(tr("File tree"), this);
filetreeDock->setWidget(m_filetree);
filetreeDock->setObjectName("filetreeDock");
databaseViewDock->hide();
addDockWidget(Qt::LeftDockWidgetArea, filetreeDock);
#endif
Histogram *histogram = new Histogram(this);
QDockWidget *histogramDock = new QDockWidget(tr("Histogram"), this);
@@ -131,38 +175,59 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
histogramDock->hide();
addDockWidget(Qt::LeftDockWidgetArea, histogramDock);
DatabaseTreeView *databaseTreeView = new DatabaseTreeView(m_database, this);
QDockWidget *databaseTreeDock = new QDockWidget(tr("Database Tree"), this);
databaseTreeDock->setObjectName("databasetreeDock");
databaseTreeDock->setWidget(databaseTreeView);
databaseTreeDock->hide();
connect(databaseTreeDock, &QDockWidget::visibilityChanged, databaseTreeView, &DatabaseTreeView::visible);
connect(databaseTreeView, &DatabaseTreeView::loadFile, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
addDockWidget(Qt::BottomDockWidgetArea, databaseTreeDock);
setWindowTitle(tr("Tenmon"));
connect(m_ringList, SIGNAL(pixmapLoaded(Image*)), this, SLOT(pixmapLoaded(Image*)));
connect(m_ringList, SIGNAL(currentImageChanged(int)), this, SLOT(updateWindowTitle()));
connect(m_ringList, SIGNAL(infoLoaded(ImageInfoData)), m_info, SLOT(setInfo(const ImageInfoData&)));
connect(m_ringList, SIGNAL(currentImageChanged(int)), m_filesystem, SLOT(selectFile(int)));
connect(m_ringList, &ImageRingList::thumbnailLoaded, m_imageGL->imageWidget(), &ImageWidget::thumbnailLoaded);
connect(m_ringList, &ImageRingList::pixmapLoaded, m_image, &ImageScrollArea::imageLoaded);
connect(m_ringList, &ImageRingList::currentImageChanged, this, &MainWindow::updateWindowTitle);
connect(m_ringList, &ImageRingList::infoLoaded, m_info, &ImageInfo::setInfo);
connect(m_ringList, &ImageRingList::currentImageChanged, m_filesystem, &FilesystemWidget::selectFile);
connect(m_ringList, &ImageRingList::thumbnailLoaded, m_image, &ImageScrollArea::thumbnailLoaded);
connect(m_ringList, &ImageRingList::pixmapLoaded, m_stretchPanel, &StretchToolbar::imageLoaded);
connect(m_ringList, &ImageRingList::pixmapLoaded, histogram, &Histogram::imageLoaded);
connect(m_imageGL->imageWidget(), &ImageWidget::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
#ifdef PLATESOLVER
connect(m_ringList, &ImageRingList::pixmapLoaded, _plateSolving, &PlateSolving::imageLoaded);
connect(_plateSolving, &PlateSolving::headerUpdated, m_ringList, &ImageRingList::reloadImage);
#endif
connect(m_image, &ImageScrollArea::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
QMenu *fileMenu = new QMenu(tr("File"), this);
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence::Open);
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, static_cast<void (MainWindow::*)()>(&MainWindow::loadFile));
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence::Save);
QAction *saveAs = fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, &MainWindow::saveAs);
fileMenu->addSeparator();
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()), Qt::Key_F5);
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()), Qt::Key_F6);
fileMenu->addAction(tr("Move marked files to trash"), this, &MainWindow::deleteMarked, QKeySequence::Delete);
#if !defined(FLATPAK) || !defined(__aarch64__)
fileMenu->addAction(tr("File manager"), this, &MainWindow::openFileManager);
#endif
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, &MainWindow::copyMarked);
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, &MainWindow::moveMarked);
QAction *deleteAction = fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
#ifdef Q_OS_MACOS
deleteAction->setShortcuts(QList<QKeySequence>({Qt::Key_Backspace, QKeySequence::Delete}));
#else
deleteAction->setShortcuts(QKeySequence::Delete);
#endif
fileMenu->addSeparator();
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
fileMenu->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir));
fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex);
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
fileMenu->addAction(tr("Batch processing"), [this](){
BatchProcessing *batchProcessing = new BatchProcessing(this);
fileMenu->addAction(tr("Batch processing"), Qt::Key_B | Qt::CTRL, [this](){
BatchProcessing *batchProcessing = new BatchProcessing(m_database, this);
batchProcessing->exec();
delete batchProcessing;
}, Qt::Key_B | Qt::CTRL);
});
fileMenu->addSeparator();
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, &MainWindow::liveMode);
liveModeAction->setCheckable(true);
QAction *exitAction = fileMenu->addAction(tr("Exit"), this, SLOT(close()));
QAction *exitAction = fileMenu->addAction(tr("Exit"), QCoreApplication::instance(), &QCoreApplication::quit, Qt::QueuedConnection);
exitAction->setShortcut(QKeySequence::Quit);
menuBar()->addMenu(fileMenu);
@@ -170,11 +235,18 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
editMenu->addAction(tr("Settings"), this, &MainWindow::showSettingsDialog);
menuBar()->addMenu(editMenu);
QMenu *navigationMenu = new QMenu(tr("Navigation"), this);
navigationMenu->addAction(prevAction);
navigationMenu->addAction(nextAction);
navigationMenu->addAction(prevSubAction);
navigationMenu->addAction(nextSubAction);
menuBar()->addMenu(navigationMenu);
QMenu *viewMenu = new QMenu(tr("View"), this);
viewMenu->addAction(tr("Zoom In"), m_imageGL, SLOT(zoomIn()), QKeySequence::ZoomIn);
viewMenu->addAction(tr("Zoom Out"), m_imageGL, SLOT(zoomOut()), QKeySequence::ZoomOut);
viewMenu->addAction(tr("Best Fit"), m_imageGL, SLOT(bestFit()), QKeySequence("Ctrl+1"));
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn, m_image, &ImageScrollArea::zoomIn);
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut, m_image, &ImageScrollArea::zoomOut);
viewMenu->addAction(tr("Best Fit"), QKeySequence("Ctrl+1"), m_image, &ImageScrollArea::bestFit);
viewMenu->addAction(tr("100%"), QKeySequence("Ctrl+0"), m_image, &ImageScrollArea::oneToOne);
viewMenu->addSeparator();
QMenu *bayerMenu = viewMenu->addMenu(tr("Bayer mask"));
QActionGroup *bayerActionGroup = new QActionGroup(this);
@@ -190,33 +262,62 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
viewMenu->addMenu(bayerMenu);
connect(bayerActionGroup, &QActionGroup::triggered, [this](QAction *action){
int data = action->data().toInt();
m_imageGL->imageWidget()->setBayerMask(data);
m_image->setBayerMask(data);
QSettings settings;
settings.setValue("mainwindow/bayermask", data);
});
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), [this](bool checked){
QStringList colormaps = {"Autumn", "Bone", "Jet", "Winter", "Rainbow", "Ocean", "Summer", "Spring", "Cool", "HSV", "Pink", "Hot", "Parula", "Magma",
"Inferno", "Plasma", "Viridis", "Cividis", "Twilight", "Twilight shifted", "Turbo", "Deepgreen"};
QMenu *colormapMenu = viewMenu->addMenu(tr("Colormap"));
QActionGroup *colormapActionGroup = new QActionGroup(this);
QImage cmImg = ImageWidget::loadColormap();
for(int i=0; i<cmImg.height(); i++)
{
QImage icon = cmImg.copy(0, i, cmImg.width(), 1).scaled(32, 32);
QAction *action = colormapActionGroup->addAction(i < colormaps.size() ? colormaps[i] : tr("User %1").arg(i - colormaps.size() + 1));
action->setIcon(QPixmap::fromImage(icon));
action->setCheckable(true); action->setData(i);
colormapMenu->addAction(action);
}
viewMenu->addMenu(colormapMenu);
connect(colormapActionGroup, &QActionGroup::triggered, [this](QAction *action){
int data = action->data().toInt();
m_image->setColormap(data);
QSettings settings;
settings.setValue("mainwindow/colormap", data);
});
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), Qt::Key_F2, [this](bool checked){
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
m_imageGL->imageWidget()->showThumbnail(checked);
m_image->allocateThumbnails(m_ringList->imageNames());
m_image->showThumbnail(checked);
if(checked)m_ringList->loadThumbnails();
else m_ringList->stopLoading();
}, Qt::Key_F2);
});
thumbnailsAction->setCheckable(true);
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), m_ringList, &ImageRingList::toggleSlideshow, Qt::Key_F3);
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), Qt::Key_F3, m_ringList, &ImageRingList::toggleSlideshow);
slideshowAction->setCheckable(true);
viewMenu->addSeparator();
auto actionList = m_stretchPanel->actions();
actionList.removeFirst();
viewMenu->addActions(actionList);
menuBar()->addMenu(viewMenu);
QMenu *selectMenu = new QMenu(tr("Select"), this);
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F7);
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, &MainWindow::markImage);
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, &MainWindow::unmarkImage);
selectMenu->addSeparator();
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
selectMenu->addAction(tr("Unmark and next"), this, SLOT(unmarkAndNext()), Qt::Key_X);
selectMenu->addAction(tr("Show marked"), this, &MainWindow::showMarkFilesDialog);
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, &MainWindow::markAndNext);
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, &MainWindow::unmarkAndNext);
selectMenu->addSeparator();
selectMenu->addAction(tr("Show marked list"), this, &MainWindow::showMarkFilesDialog);
QAction *openMarked = selectMenu->addAction(tr("Open marked"), m_ringList, &ImageRingList::setMarked);
menuBar()->addMenu(selectMenu);
fileMenu->insertAction(saveAs, openMarked);
QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
/*QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
QActionGroup *analyzeGroup = new QActionGroup(this);
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
static QAction* lastAction = nullptr;
@@ -239,21 +340,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
connect(peakAction, SIGNAL(toggled(bool)), this, SLOT(peakFinder(bool)));
connect(starAction, SIGNAL(toggled(bool)), this, SLOT(starFinder(bool)));
analyzeMenu->addActions({statsAction, peakAction, starAction});
//menuBar()->addMenu(analyzeMenu);
menuBar()->addMenu(analyzeMenu);*/
QMenu *dockMenu = new QMenu(tr("Docks"), this);
dockMenu->addAction(infoDock->toggleViewAction());
dockMenu->addAction(m_stretchPanel->toggleViewAction());
dockMenu->addAction(navigationToolbar->toggleViewAction());
dockMenu->addAction(filesystemDock->toggleViewAction());
dockMenu->addAction(databaseViewDock->toggleViewAction());
dockMenu->addAction(filetreeDock->toggleViewAction());
dockMenu->addAction(databaseTreeDock->toggleViewAction());
if(filetreeDock)dockMenu->addAction(filetreeDock->toggleViewAction());
dockMenu->addAction(histogramDock->toggleViewAction());
#ifdef PLATESOLVER
dockMenu->addAction(_plateSolving->toggleViewAction());
#endif
menuBar()->addMenu(dockMenu);
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
helpMenu->addAction(tr("Help"), [this]{ HelpDialog help(this); help.exec(); }, QKeySequence::HelpContents);
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog *help = new HelpDialog(this); help->show(); });
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
helpMenu->addAction(tr("Check for update"), this, &MainWindow::checkNewVersion);
setupSigterm();
QSettings settings;
@@ -272,6 +379,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
case 3:
bggrAction->setChecked(true); break;
}
int colormap = settings.value("mainwindow/colormap", 4).toInt();
if(colormap >= 0 && colormap < colormapActionGroup->actions().size())
colormapActionGroup->actions().at(colormap)->setChecked(true);
m_image->setBayerMask(bayermask);
m_image->setColormap(colormap);
QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
if(standardLocations.size())
@@ -279,22 +392,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
QStringList args = QCoreApplication::arguments();
args.removeFirst();
for(auto &arg : args)
{
QFileInfo info(arg);
if(info.exists())
{
m_ringList->setFile(info.canonicalFilePath());
updateWindowTitle();
_lastDir = info.absoluteDir().absolutePath();
settings.setValue("mainwindow/lastdir", _lastDir);
break;
}
}
m_imageGL->setFocus();
m_image->setFocus();
// workaround for nasty wayland backend bug https://bugreports.qt.io/browse/QTBUG-87332
if(static_cast<QGuiApplication*>(QCoreApplication::instance())->platformName() == "wayland")
@@ -302,7 +400,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
infoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
filesystemDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
databaseViewDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
if(filetreeDock)filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
if(_plateSolving)_plateSolving->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
m_stretchPanel->setFloatable(false);
}
}
@@ -312,32 +411,6 @@ MainWindow::~MainWindow()
delete m_database;
}
void MainWindow::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_Left:
case Qt::Key_Up:
m_ringList->decrement();
break;
case Qt::Key_Right:
case Qt::Key_Down:
m_ringList->increment();
break;
default:
event->ignore();
break;
}
if(event->isAccepted())
updateWindowTitle();
}
void MainWindow::keyReleaseEvent(QKeyEvent *event)
{
event->ignore();
}
void MainWindow::setupSigterm()
{
#ifdef __linux__
@@ -354,7 +427,7 @@ void MainWindow::setupSigterm()
::socketpair(AF_UNIX, SOCK_STREAM, 0, socketPair);
socketNotifier = new QSocketNotifier(socketPair[1], QSocketNotifier::Read, this);
connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(socketNotify()));
connect(socketNotifier, &QSocketNotifier::activated, this, &MainWindow::socketNotify);
#endif
}
@@ -387,6 +460,9 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
if(!dest.isEmpty() && dir.exists())
{
int i = 0;
int missing = 0;
bool overwriteAll = false;
bool skipAll = false;
QStringList files = m_database->getMarkedFiles();
QProgressDialog progress(copy ? tr("Copying") : tr("Moving"), tr("Cancel"), 0, files.size(), this);
progress.setWindowModality(Qt::WindowModal);
@@ -398,8 +474,42 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
QFile srcFile(file);
QFile dstFile(dir.absoluteFilePath(info.fileName()));
if(dstFile.exists())
if(!srcFile.exists())
{
missing++;
continue;
}
if(dstFile.exists())
{
if(skipAll)
{
continue;
}
else if(overwriteAll)
{
dstFile.remove();
}
else
{
QMessageBox::StandardButton button = QMessageBox::question(this, tr("Overwrite file?"), tr("Destination file %1 already exists. Overwrite?").arg(dstFile.fileName()),
QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll);
switch (button)
{
case QMessageBox::YesToAll:
overwriteAll = true;
case QMessageBox::Yes:
dstFile.remove();
break;
case QMessageBox::NoToAll:
skipAll = true;
case QMessageBox::No:
continue;
default:
break;
}
}
}
if(progress.wasCanceled())
return;
@@ -438,6 +548,8 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
progress.setValue(i++);
}
m_database->clearMarkedFiles();
if(missing)
QMessageBox::information(this, tr("Missing marked files"), tr("%1 marked files were missing. They were skipped.").arg(missing));
}
}
@@ -450,14 +562,6 @@ void MainWindow::socketNotify()
socketNotifier->setEnabled(true);
}
void MainWindow::pixmapLoaded(Image *image)
{
if(image->rawImage())
{
m_imageGL->setImage(image);
}
}
void MainWindow::loadFile()
{
QString file = QFileDialog::getOpenFileName(this,
@@ -472,6 +576,8 @@ void MainWindow::loadFile(const QString &path)
if(!path.isEmpty())
{
QFileInfo info(path);
if(info.exists() && info.isReadable())
{
m_ringList->setFile(info.canonicalFilePath());
updateWindowTitle();
if(info.isDir())
@@ -480,7 +586,19 @@ void MainWindow::loadFile(const QString &path)
_lastDir = info.canonicalPath();
QSettings settings;
settings.setValue("mainwindow/lastdir", _lastDir);
if(settings.value("settings/bestfit", false).toBool())
m_image->bestFit();
}
else
{
qWarning() << "File doesn't exist or is not readable";
}
}
}
void MainWindow::loadFiles(const QStringList &paths)
{
m_ringList->setFiles(paths);
}
void MainWindow::loadFile(int row)
@@ -528,45 +646,50 @@ void MainWindow::reindex()
void MainWindow::saveAs()
{
QString selectedFilter;
ImagePtr ptr = m_ringList->currentImage();
if(!ptr)return;
QFileInfo srcFile(ptr->name());
QString file = QFileDialog::getSaveFileName(this,
tr("Save as"),
_lastDir,
_lastDir + "/" + srcFile.baseName(),
_saveFilter,
&selectedFilter);
auto filterToFormat = [](const QString &file, const QString &filter) -> const char*
auto filterToFormat = [](const QString &file, const QString &filter) -> const QString
{
QString suffix = QFileInfo(file).suffix();
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "JPEG";
if(!suffix.compare("png", Qt::CaseInsensitive))return "PNG";
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "FITS";
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "XISF";
if(filter.contains("png"))return "PNG";
if(filter.contains("fits"))return "FITS";
if(filter.contains("xisf"))return "XISF";
return "JPEG";
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "jpeg";
if(!suffix.compare("png", Qt::CaseInsensitive))return "png";
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "fits";
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "xisf";
if(filter.contains("png"))return "png";
if(filter.contains("fits"))return "fits";
if(filter.contains("xisf"))return "xisf";
QRegularExpression suf("\\(\\*\\.([a-zA-Z]+).*\\)");
auto match = suf.match(filter);
if(match.hasMatch())
return match.captured(1);
return "jpeg";
};
if(!file.isEmpty())
{
auto button = QMessageBox::question(this, tr("Apply stretch?"), tr("Apply current stretch function to image?"));
QString format = filterToFormat(file, selectedFilter);
if(format == "FITS" || format == "XISF")
{
convert(file, format);
}
else
{
QImage img = m_imageGL->imageWidget()->renderToImage();
if(!img.isNull())
img.save(file, filterToFormat(file, selectedFilter));
}
convert(file, format, button == QMessageBox::Yes);
}
}
void MainWindow::convert(const QString &outfile, const QString &format)
void MainWindow::convert(const QString &outfile, const QString &format, bool stretch)
{
QString file = m_ringList->currentImage()->name();
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format));
ConvertRunable::ConvertParams param;
param.stretch = stretch;
param.mtf = m_stretchPanel->params();
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format, param));
}
void MainWindow::markImage()
@@ -699,13 +822,74 @@ void MainWindow::exportCSV()
m_databaseView->exportCSV(file);
}
void MainWindow::checkNewVersion()
{
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl("https://gitea.nouspiro.space/api/v1/repos/nou/tenmon/releases/latest"));
request.setRawHeader("accept", "application/json");
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, [this, manager, reply](){
QJsonParseError error;
QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error);
if(json.isObject() && json.object().contains("tag_name"))
{
QString tag = json.object().value("tag_name").toString();
QString version = getVersion();
if(version >= tag)
QMessageBox::information(this, tr("Update check"), tr("You have newest version"));
else
{
if(QMessageBox::question(this, tr("Update check"), tr("New version %1 is available. Do you want to download it now?").arg(tag)) == QMessageBox::Yes)
{
QUrl url(json.object().value("html_url").toString());
qDebug() << "Opening url" << url;
if(url.host() == "gitea.nouspiro.space")
QDesktopServices::openUrl(url);
}
}
}
else
{
QMessageBox::warning(this, tr("Update check"), tr("Failed to check version"));
}
reply->deleteLater();
manager->deleteLater();
});
}
void MainWindow::openFileManager()
{
#if !defined(FLATPAK) || !defined(__aarch64__)
FileManager *filemanager = new FileManager(_openSuffix);
connect(filemanager, &FileManager::openFile, this, static_cast<void (MainWindow::*)(const QString&)>(&MainWindow::loadFile));
filemanager->show();
#endif
}
void MainWindow::runScript(const QString &script, const QString &outdir, const QStringList &paths, const QString &arg, bool exit)
{
BatchProcessing *batchProcessing = new BatchProcessing(m_database, this);
batchProcessing->setOutputDir(outdir);
batchProcessing->setPaths(paths);
if(exit)batchProcessing->hide();
QTimer::singleShot(500, [batchProcessing, script, exit, arg](){
batchProcessing->runScript(script, arg, exit);
batchProcessing->exec();
delete batchProcessing;
if(exit)QCoreApplication::exit();
});
}
void MainWindow::updateWindowTitle()
{
ImagePtr ptr = m_ringList->currentImage();
if(ptr)
{
QFileInfo info(ptr->name());
QString title = info.fileName();
QDir dir(m_ringList->currentDir());
QString title = dir.relativeFilePath(ptr->name());
if(ptr->info().num > 1)
title += QString(" [%1/%2]").arg(ptr->info().index + 1).arg(ptr->info().num);
if(m_database->isMarked(ptr->name()))
title += " *";
setWindowTitle(title);
+12 -8
View File
@@ -4,19 +4,19 @@
#include <QMainWindow>
#include <QSocketNotifier>
#include "imageringlist.h"
#include "imagescrollarea.h"
#include "database.h"
#include "imageinfo.h"
#include "imagescrollareagl.h"
#include "imagescrollarea.h"
#include "filesystemwidget.h"
#include "stretchtoolbar.h"
#include "databaseview.h"
#include "platesolving.h"
#include "databasetree.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
ImageScrollArea *m_image;
ImageScrollAreaGL *m_imageGL;
ImageRingList *m_ringList;
StretchToolbar *m_stretchPanel;
Database *m_database;
@@ -24,36 +24,37 @@ class MainWindow : public QMainWindow
FilesystemWidget *m_filesystem;
Filetree *m_filetree;
DataBaseView *m_databaseView;
PlateSolving *_plateSolving = nullptr;
DatabaseTree *_databaseTree = nullptr;
static int socketPair[2];
QSocketNotifier *socketNotifier;
QString _lastDir;
bool _maximized;
QString _openFilter;
QString _saveFilter;
QSet<QString> _openSuffix;
public:
MainWindow(QWidget *parent = 0);
~MainWindow() override;
protected:
void keyPressEvent(QKeyEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
void setupSigterm();
static void signalHandler(int);
void closeEvent(QCloseEvent *event) override;
void copyOrMove(bool copy);
void copyOrMove(bool copy, const QString &dest);
protected slots:
public slots:
void socketNotify();
void updateWindowTitle();
void pixmapLoaded(Image *image);
void loadFile();
void loadFile(const QString &path);
void loadFiles(const QStringList &paths);
void loadFile(int row);
void loadDir();
void indexDir();
void indexDir(const QString &dir);
void reindex();
void saveAs();
void convert(const QString &outfile, const QString &format);
void convert(const QString &outfile, const QString &format, bool stretch);
void markImage();
void unmarkImage();
void markAndNext();
@@ -68,6 +69,9 @@ protected slots:
void showMarkFilesDialog();
void showSettingsDialog();
void exportCSV();
void checkNewVersion();
void openFileManager();
void runScript(const QString &script, const QString &outdir, const QStringList &paths, const QString &arg, bool exit);
};
#endif // MAINWINDOW_H
+1 -1
View File
@@ -60,6 +60,6 @@ void MarkedFiles::clearSelected()
void MarkedFiles::clearAll()
{
QSqlDatabase db = QSqlDatabase::database();
db.exec("DELETE FROM files");
QSqlQuery("DELETE FROM files", db);
m_model->select();
}
View File
+11
View File
@@ -0,0 +1,11 @@
#ifndef MTFPARAM_H
#define MTFPARAM_H
struct MTFParam
{
float blackPoint[3] = {0.0f, 0.0f, 0.0f};
float midPoint[3] = {0.5f, 0.5f, 0.5f};
float whitePoint[3] = {1.0f, 1.0f, 1.0f};
};
#endif // MTFPARAM_H
+203
View File
@@ -0,0 +1,203 @@
#include "platesolving.h"
#include <QSettings>
#include <QMessageBox>
#include "ui_platesolving.h"
#include "solver.h"
#include "imageringlist.h"
#include "platesolvingsettings.h"
PlateSolving::PlateSolving(QWidget *parent)
: QDockWidget(parent)
, _ui(new Ui::PlateSolving)
{
_ui->setupUi(this);
_solver = new Solver(this);
QSettings settings;
_solver->setIndexFolder(settings.value("platesolving/indexPath", Solver::getTenmonIndexPath()).toString());
auto profiles = StellarSolver::getBuiltInProfiles();
int profileIdx = settings.value("platesolving/profile", 0).toInt();
_solver->setParameters(profiles[profileIdx]);
for(auto &profile : profiles)
{
_ui->profileComboBox->addItem(profile.listName);
}
_ui->profileComboBox->setCurrentIndex(profileIdx);
_ui->profileComboBox->setToolTip(profiles[profileIdx].description);
_ui->scaleUnit->setCurrentIndex(settings.value("platesolving/scaleUnit", 1).toInt());
connect(_ui->profileComboBox, &QComboBox::currentIndexChanged, [this](int index){
auto profiles = StellarSolver::getBuiltInProfiles();
_solver->setParameters(profiles[index]);
_ui->profileComboBox->setToolTip(profiles[index].description);
QSettings settings;
settings.setValue("platesolving/profile", index);
});
connect(_ui->extractButton, &QPushButton::clicked, this, &PlateSolving::extract);
connect(_ui->solveButton, &QPushButton::clicked, this, &PlateSolving::solve);
connect(_ui->settingsButton, &QPushButton::clicked, this, &PlateSolving::settings);
connect(_ui->abortButton, &QPushButton::clicked, this, &PlateSolving::abort);
connect(_ui->updateButton, &QPushButton::clicked, this, &PlateSolving::updateHeader);
connect(_ui->raStart, &QDoubleSpinBox::valueChanged, [this](double val){ _ui->raLabel->setText("RA " + SkyPoint::toHMS(val)); });
connect(_solver, &Solver::solvingDone, this, &PlateSolving::solvingDone);
connect(_solver, &Solver::extractionDone, this, &PlateSolving::extractionDone);
connect(_solver, &Solver::logOutput, [this](const QString &log){ _ui->log->appendPlainText(log); });
connect(_solver, &Solver::headerUpdated, this, &PlateSolving::headerUpdated);
}
PlateSolving::~PlateSolving()
{
QSettings settings;
settings.setValue("platesolving/profile", _ui->profileComboBox->currentIndex());
settings.setValue("platesolving/scaleUnit", _ui->scaleUnit->currentIndex());
delete _ui;
}
void PlateSolving::extract()
{
if(!_rawImage)return;
_ui->solveButton->setDisabled(true);
_ui->extractButton->setDisabled(true);
_ui->log->clear();
_solver->loadImage(_rawImage, _path);
_solvingTime.start();
_solver->extractSources(_ui->withHFR->isChecked());
}
void PlateSolving::extractionDone()
{
auto stars = _solver->getStars();
float a = 0;
float b = 0;
float hfr = 0;
for(auto &star : stars)
{
a += star.a;
b += star.b;
hfr += star.HFR;
}
if(size_t size = stars.size())
{
a /= size;
b /= size;
hfr /= size;
}
_ui->stars->setText(QString::number(stars.size()));
_ui->hfr->setText(QString("%1pix Ecc:%2").arg(hfr).arg(std::sqrt(1 - (b*b)/(a*a))));
_ui->log->appendPlainText(QString("Extraction finished in %1 ms").arg(_solvingTime.elapsed()));
_ui->solveButton->setDisabled(false);
_ui->extractButton->setDisabled(false);
}
void PlateSolving::solve()
{
if(!_rawImage)return;
_ui->solveButton->setDisabled(true);
_ui->extractButton->setDisabled(true);
_ui->log->clear();
_solver->loadImage(_rawImage, _path);
if(_ui->usePosition->isChecked())
_solver->setSearchPosition(_ui->raStart->value(), _ui->decStart->value());
if(_ui->useScale->isChecked())
{
SSolver::ScaleUnits scaleUnit;
switch(_ui->scaleUnit->currentIndex())
{
default:
case 0:
scaleUnit = SSolver::ScaleUnits::DEG_WIDTH; break;
case 1:
scaleUnit = SSolver::ScaleUnits::ARCMIN_WIDTH; break;
case 2:
scaleUnit = SSolver::ScaleUnits::ARCSEC_PER_PIX; break;
case 3:
scaleUnit = SSolver::ScaleUnits::FOCAL_MM; break;
}
_solver->setSearchScale(_ui->fovLow->value(), _ui->fovHigh->value(), scaleUnit);
}
_solvingTime.start();
_solver->solveImage();
}
void PlateSolving::solvingDone()
{
_ui->solveButton->setDisabled(false);
_ui->extractButton->setDisabled(false);
auto solution = _solver->getSolution();
_ui->ra->setText(SkyPoint::toHMS(solution.ra / 15.0));
_ui->dec->setText(SkyPoint::toDMS(solution.dec));
_ui->orientation->setText(QString::number(solution.orientation) + "°");
_ui->fieldWidth->setText(QString::number(solution.fieldWidth) + "'");
_ui->fieldHeight->setText(QString::number(solution.fieldHeight) + "\"");
_ui->pixelScale->setText(QString::number(solution.pixscale) + "\"/pix");
_ui->log->appendPlainText(QString("Solving finished in %1 ms").arg(_solvingTime.elapsed()));
}
void PlateSolving::abort()
{
_solver->abort();
}
void PlateSolving::updateHeader()
{
QString error;
if(!_solver->updateHeader(error))
QMessageBox::warning(this, tr("Header update failed"), error);
}
void PlateSolving::imageLoaded(Image *image)
{
if(image && image->rawImage())
{
_rawImage = image->rawImage();
_path = image->name();
_ui->ra->clear();
_ui->dec->clear();
_ui->orientation->clear();
_ui->fieldWidth->clear();
_ui->fieldHeight->clear();
_ui->pixelScale->clear();
_ui->hfr->clear();
_ui->stars->clear();
const ImageInfoData &info = image->info();
SkyPointScale pointScale = info.getCenterRaDec();
if(!std::isnan(pointScale.point.RA()) && !std::isnan(pointScale.point.DEC()))
{
_ui->raStart->setValue(pointScale.point.RAHour());
_ui->decStart->setValue(pointScale.point.DEC());
_ui->usePosition->setChecked(true);
}
else
{
_ui->usePosition->setChecked(false);
}
if(pointScale.scaleValid)
{
_ui->scaleUnit->setCurrentIndex(2);
_ui->fovLow->setValue(pointScale.scaleLow);
_ui->fovHigh->setValue(pointScale.scaleHigh);
_ui->useScale->setChecked(true);
}
else
{
_ui->useScale->setChecked(false);
}
}
}
void PlateSolving::settings()
{
PlateSolvingSettings settings(this);
settings.exec();
_solver->setIndexFolder(settings.indexDirectory());
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef PLATESOLVING_H
#define PLATESOLVING_H
#include <QElapsedTimer>
#include <QDockWidget>
class Solver;
class RawImage;
class Image;
namespace Ui {
class PlateSolving;
}
class PlateSolving : public QDockWidget
{
Q_OBJECT
Solver *_solver;
std::shared_ptr<RawImage> _rawImage;
QString _path;
QElapsedTimer _solvingTime;
public:
explicit PlateSolving(QWidget *parent = nullptr);
~PlateSolving();
public slots:
void extract();
void extractionDone();
void solve();
void solvingDone();
void abort();
void updateHeader();
void imageLoaded(Image *image);
void settings();
signals:
void headerUpdated(const QString &path);
private:
Ui::PlateSolving *_ui;
};
#endif // PLATESOLVING_H
+364
View File
@@ -0,0 +1,364 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlateSolving</class>
<widget class="QDockWidget" name="PlateSolving">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>860</width>
<height>700</height>
</rect>
</property>
<property name="windowTitle">
<string>Plate Solving</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Profile</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="profileComboBox"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Start point</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="3">
<widget class="QComboBox" name="scaleUnit">
<item>
<property name="text">
<string>Degree width</string>
</property>
</item>
<item>
<property name="text">
<string>Arcmin width</string>
</property>
</item>
<item>
<property name="text">
<string>Arcsec per pixel</string>
</property>
</item>
<item>
<property name="text">
<string>35 mm equivalent focal length</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="usePosition">
<property name="text">
<string>Use position</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QDoubleSpinBox" name="fovHigh">
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>10000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_11">
<property name="text">
<string>DEC</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="raStart">
<property name="suffix">
<string> h</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>24.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_13">
<property name="text">
<string>High</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="fovLow">
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="useScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Use scale</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="4">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QDoubleSpinBox" name="decStart">
<property name="suffix">
<string> deg</string>
</property>
<property name="minimum">
<double>-90.000000000000000</double>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Low </string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="raLabel">
<property name="text">
<string>RA</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Unit</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Solution</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>RA</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="ra">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_5">
<property name="text">
<string>DEC</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="dec">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Field width</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="fieldWidth">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Field height</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QLineEdit" name="fieldHeight">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Orientation</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="orientation">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Pixel scale</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLineEdit" name="pixelScale">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Stars</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="stars">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>HFR</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="hfr">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="log">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QPushButton" name="settingsButton">
<property name="text">
<string>Settings</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="extractButton">
<property name="text">
<string>Extract</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="solveButton">
<property name="text">
<string>Solve</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="withHFR">
<property name="text">
<string>Extract with HFR</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="abortButton">
<property name="text">
<string>Abort</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="updateButton">
<property name="text">
<string>Update FITS header</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
+135
View File
@@ -0,0 +1,135 @@
#include "platesolvingsettings.h"
#include "ui_platesolvingsettings.h"
#include <QSettings>
#include <QFileDialog>
#include "solver.h"
PlateSolvingSettings::PlateSolvingSettings(QWidget *parent) : QDialog(parent)
, _ui(new Ui::PlateSolvingSettings)
{
_ui->setupUi(this);
_download = new HttpDownloader(this);
connect(_download, &HttpDownloader::progress, this, &PlateSolvingSettings::progress);
connect(_ui->stopDownloadButton, &QPushButton::clicked, _download, &HttpDownloader::abort);
connect(_ui->scale01, &QCheckBox::clicked, [this](){ if(_ui->scale01->isChecked())_download->downloadIndex(1); });
connect(_ui->scale02, &QCheckBox::clicked, [this](){ if(_ui->scale02->isChecked())_download->downloadIndex(2); });
connect(_ui->scale03, &QCheckBox::clicked, [this](){ if(_ui->scale03->isChecked())_download->downloadIndex(3); });
connect(_ui->scale04, &QCheckBox::clicked, [this](){ if(_ui->scale04->isChecked())_download->downloadIndex(4); });
connect(_ui->scale05, &QCheckBox::clicked, [this](){ if(_ui->scale05->isChecked())_download->downloadIndex(5); });
connect(_ui->scale06, &QCheckBox::clicked, [this](){ if(_ui->scale06->isChecked())_download->downloadIndex(6); });
connect(_ui->scale07, &QCheckBox::clicked, [this](){ if(_ui->scale07->isChecked())_download->downloadIndex(7); });
connect(_ui->scale08, &QCheckBox::clicked, [this](){ if(_ui->scale08->isChecked())_download->downloadIndex(8); });
connect(_ui->scale09, &QCheckBox::clicked, [this](){ if(_ui->scale09->isChecked())_download->downloadIndex(9); });
connect(_ui->scale10, &QCheckBox::clicked, [this](){ if(_ui->scale10->isChecked())_download->downloadIndex(10); });
connect(_ui->scale11, &QCheckBox::clicked, [this](){ if(_ui->scale11->isChecked())_download->downloadIndex(11); });
connect(_ui->scale12, &QCheckBox::clicked, [this](){ if(_ui->scale12->isChecked())_download->downloadIndex(12); });
connect(_ui->scale13, &QCheckBox::clicked, [this](){ if(_ui->scale13->isChecked())_download->downloadIndex(13); });
connect(_ui->scale14, &QCheckBox::clicked, [this](){ if(_ui->scale14->isChecked())_download->downloadIndex(14); });
connect(_ui->scale15, &QCheckBox::clicked, [this](){ if(_ui->scale15->isChecked())_download->downloadIndex(15); });
connect(_ui->scale16, &QCheckBox::clicked, [this](){ if(_ui->scale16->isChecked())_download->downloadIndex(16); });
connect(_ui->scale17, &QCheckBox::clicked, [this](){ if(_ui->scale17->isChecked())_download->downloadIndex(17); });
connect(_ui->scale18, &QCheckBox::clicked, [this](){ if(_ui->scale18->isChecked())_download->downloadIndex(18); });
connect(_ui->scale19, &QCheckBox::clicked, [this](){ if(_ui->scale19->isChecked())_download->downloadIndex(19); });
QSettings settings;
_ui->indexPaths->addItems(settings.value("platesolving/indexPaths", Solver::getIndexPaths()).toStringList());
_ui->indexPaths->setCurrentText(settings.value("platesolving/indexPath", Solver::getTenmonIndexPath()).toString());
connect(_ui->addButton, &QPushButton::clicked, [this](){
QString path = QFileDialog::getExistingDirectory(this, tr("Index files directory"), Solver::getTenmonIndexPath());
if(!path.isEmpty())
{
bool contain = false;
for(int i=0; i<_ui->indexPaths->count(); i++)
{
if(path == _ui->indexPaths->itemText(i))
{
contain = true;
break;
}
}
if(!contain)_ui->indexPaths->addItem(path);
}
});
connect(_ui->removeButton, &QPushButton::clicked, [this](){
int current = _ui->indexPaths->currentIndex();
if(current > 0)_ui->indexPaths->removeItem(current);
});
_watcher = new QFileSystemWatcher(this);
_watcher->addPath(Solver::getTenmonIndexPath());
connect(_watcher, &QFileSystemWatcher::directoryChanged, this, &PlateSolvingSettings::checkIndexFiles);
connect(_ui->indexPaths, &QComboBox::currentTextChanged, [this](const QString &text){
_watcher->removePaths(_watcher->directories());
_watcher->addPath(text);
});
connect(_ui->indexPaths, &QComboBox::currentIndexChanged, [this](int index){
_ui->indexFilesGroup->setEnabled(index == 0);
});
checkIndexFiles();
}
PlateSolvingSettings::~PlateSolvingSettings()
{
QSettings settings;
settings.setValue("platesolving/indexPath", _ui->indexPaths->currentText());
QStringList paths;
for(int i=0; i<_ui->indexPaths->count(); i++)
paths.append(_ui->indexPaths->itemText(i));
settings.setValue("platesolving/indexPaths", paths);
delete _ui;
}
void PlateSolvingSettings::checkIndexFiles()
{
QString indexDir = Solver::getTenmonIndexPath() + "/";
auto checkScale = [indexDir](QCheckBox *box, int scale)
{
bool all = true;
QStringList files = HttpDownloader::indexFileNames(scale);
for(auto &file : files)
if(!QFile::exists(indexDir + file))
{
all = false;
break;
}
box->setChecked(all);
if(all)box->setStyleSheet("color: green; font: bold;");
else box->setStyleSheet("");
};
checkScale(_ui->scale01, 1);
checkScale(_ui->scale02, 2);
checkScale(_ui->scale03, 3);
checkScale(_ui->scale04, 4);
checkScale(_ui->scale05, 5);
checkScale(_ui->scale06, 6);
checkScale(_ui->scale07, 7);
checkScale(_ui->scale08, 8);
checkScale(_ui->scale09, 9);
checkScale(_ui->scale10, 10);
checkScale(_ui->scale11, 11);
checkScale(_ui->scale12, 12);
checkScale(_ui->scale13, 13);
checkScale(_ui->scale14, 14);
checkScale(_ui->scale15, 15);
checkScale(_ui->scale16, 16);
checkScale(_ui->scale17, 17);
checkScale(_ui->scale18, 18);
checkScale(_ui->scale19, 19);
}
QString PlateSolvingSettings::indexDirectory() const
{
return _ui->indexPaths->currentText();
}
void PlateSolvingSettings::progress(int percent, int files)
{
_ui->filesRemaining->setText(tr("%1 files").arg(files));
_ui->downloadProgressbar->setValue(percent);
}
+28
View File
@@ -0,0 +1,28 @@
#ifndef PLATESOLVINGSETTINGS_H
#define PLATESOLVINGSETTINGS_H
#include <QDialog>
#include <QFileSystemWatcher>
#include "httpdownloader.h"
namespace Ui {
class PlateSolvingSettings;
}
class PlateSolvingSettings : public QDialog
{
Q_OBJECT
HttpDownloader *_download;
QFileSystemWatcher *_watcher;
public:
explicit PlateSolvingSettings(QWidget *parent = nullptr);
~PlateSolvingSettings();
void checkIndexFiles();
QString indexDirectory() const;
protected slots:
void progress(int percent, int files);
private:
Ui::PlateSolvingSettings *_ui;
};
#endif // PLATESOLVINGSETTINGS_H
+229
View File
@@ -0,0 +1,229 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlateSolvingSettings</class>
<widget class="QDialog" name="PlateSolvingSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>787</width>
<height>479</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="indexPaths">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeButton">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plate solving need index files in order to solve an image. You can download them here by clicking on check box. It will download them into default location. Or you can reuse &lt;span style=&quot; font-weight:700;&quot;&gt;any&lt;/span&gt; index files (not only listed bellow) from astrometry.net by adding path pointing to them. &lt;a href=&quot;https://astrometrynet.readthedocs.io/en/latest/readme.html#getting-index-files&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;More details about index files.&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;It is required to download index files that cover 100%-50% field of view and recomended 100%-10%. So for images with 70' field of view it is required to download index files in 30'-85' and recomended 4'-85'.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="indexFilesGroup">
<property name="title">
<string>Index files</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="5" column="0">
<widget class="QCheckBox" name="scale14">
<property name="text">
<string>240' - 340'index-4114.fits (1.4 MiB)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="scale17">
<property name="text">
<string>680' - 1000' index-4117.fits (242 kiB)</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="scale16">
<property name="text">
<string>480' - 680' index-4116.fits (400 kiB)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="scale18">
<property name="text">
<string>1000' - 1400' index-4118.fits (183 kiB)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="scale15">
<property name="text">
<string>340' - 480' index-4115.fits (723 kiB)</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="scale19">
<property name="text">
<string>1400' - 2000' index-4119.fits (141 kiB)</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="scale12">
<property name="text">
<string>120' - 170' index-4112.fits (5.1MiB)</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="scale13">
<property name="text">
<string>170' - 240' index-4113.fits (2.7MiB)</string>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QCheckBox" name="scale11">
<property name="text">
<string>85' - 120' index-4111.fits (9.8 MiB)</string>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QCheckBox" name="scale10">
<property name="text">
<string>60' - 85' index-4110.fits (24 MiB)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="scale09">
<property name="text">
<string>42' - 60' index-4109.fits (48 MiB)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="scale08">
<property name="text">
<string>30' - 42' index-4108.fits (91 MiB)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="scale07">
<property name="text">
<string>22' - 30' index-4107.fits (158 MiB)</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="scale06">
<property name="text">
<string>16' - 22' index-5206-*.fits (294 MiB)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="scale05">
<property name="text">
<string>11' - 16' index-5205-*.fits (587 MiB)</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="scale04">
<property name="text">
<string>8' - 11' index-5204-*.fits (1.2 GiB)</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="scale03">
<property name="text">
<string>4.0' - 5.6' index-5203-*.fits (2.3 GiB)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="scale02">
<property name="text">
<string>5.6' - 8.0' index-5202-*.fits (4.6 GiB)</string>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QCheckBox" name="scale01">
<property name="text">
<string>2.0' - 2.8' index-5201-*.fits (8.9 GiB)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="filesRemaining">
<property name="text">
<string>0 files</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="downloadProgressbar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopDownloadButton">
<property name="text">
<string>Stop download</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+1312
View File
File diff suppressed because it is too large Load Diff
+27 -5
View File
@@ -1,13 +1,16 @@
#ifndef RAWIMAGE_H
#define RAWIMAGE_H
#include "libxisf.h"
#include <vector>
#include <algorithm>
#include <memory>
#include <stdint.h>
#include <math.h>
#include <memory.h>
#ifndef NO_QT
#include <QImage>
#endif
#include "mtfparam.h"
extern int THUMB_SIZE;
extern int THUMB_SIZE_BORDER;
@@ -45,6 +48,7 @@ public:
UINT8,
UINT16,
UINT32,
FLOAT16,
FLOAT32,
FLOAT64,
};
@@ -65,6 +69,8 @@ protected:
std::unique_ptr<PixelType[]> m_original;
uint32_t m_width = 0;
uint32_t m_height = 0;
uint32_t m_origWidth = 0;
uint32_t m_origHeight = 0;
uint32_t m_channels = 0;
uint32_t m_ch = 0;
DataType m_type = UINT8;
@@ -72,24 +78,27 @@ protected:
float m_thumbAspect = 0.0;
Stats m_stats;
bool m_planar = false;
std::vector<uint8_t> m_iccProfile;
std::vector<uint16_t> m_lut;// actually qfloat16
void allocate(uint32_t w, uint32_t h, uint32_t ch, DataType type);
public:
RawImage();
RawImage(uint32_t w, uint32_t h, uint32_t ch, DataType type);
RawImage(const RawImage &d);
RawImage(RawImage &&d);
#ifndef NO_QT
RawImage(const QImage &img);
#endif
const RawImage::Stats& imageStats() const;
void calcStats();
void rect(int &x, int &y, int w, int h, std::vector<double> &r) const;
int findPeaks(double background, double distance, std::vector<Peak> &peaks) const;
uint32_t width() const;
uint32_t height() const;
uint32_t channels() const;
uint32_t size() const;
uint64_t size() const;
DataType type() const;
uint32_t norm() const;
uint32_t widthBytes() const;
uint64_t widthBytes() const;
uint32_t widthSamples() const;
void* data();
const void* data() const;
void* data(uint32_t row, uint32_t col = 0);
@@ -100,17 +109,30 @@ public:
void setPlanar();
void convertToThumbnail();
void convertToGLFormat();
void convertToType(RawImage::DataType type);
float thumbAspect() const;
bool pixel(int x, int y, double &r, double &g, double &b) const;
void resize(uint32_t w, uint32_t h);
void resizeInt(int downsample, bool avg);
std::pair<float, float> unitScale() const;
void flip();
static std::shared_ptr<RawImage> fromPlanar(const RawImage &img);
static std::shared_ptr<RawImage> fromPlanar(const void *pixels, uint32_t w, uint32_t h, uint32_t ch, DataType type);
std::shared_ptr<RawImage> toPlanar();
static size_t typeSize(DataType type);
std::vector<RawImage> split() const;
bool valid() const;
#ifndef NO_QT
void setICCProfile(const QByteArray &icc);
void setICCProfile(const LibXISF::ByteArray &icc);
void convertTosRGB();
void generateLUT();
#endif
void applySTF(const MTFParam &mtfParams);
MTFParam calcMTFParams(bool linked = false, bool debayer = false) const;
const std::vector<uint16_t>& getLUT() const;
};
//Q_DECLARE_SMART_POINTER_METATYPE(std::shared_ptr);
+8 -8
View File
@@ -1,7 +1,7 @@
#include "rawimage.h"
#ifdef __SSE2__
#include <x86intrin.h>
#include <cstdint>
#include <limits>
template<typename T, int ch>
void fromPlanarSSE(const void *in, void *out, size_t count)
@@ -21,7 +21,7 @@ void fromPlanarSSE(const void *in, void *out, size_t count)
__m128i r = _mm_loadu_si128(_in[0] + i);
__m128i g = _mm_loadu_si128(_in[1] + i);
__m128i b = _mm_loadu_si128(_in[2] + i);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3] + i);
__m128i d1 = _mm_unpacklo_epi8(r, b);
__m128i d2 = _mm_unpacklo_epi8(g, a);
@@ -43,7 +43,7 @@ void fromPlanarSSE(const void *in, void *out, size_t count)
__m128i r = _mm_loadu_si128(_in[0] + i);
__m128i g = _mm_loadu_si128(_in[1] + i);
__m128i b = _mm_loadu_si128(_in[2] + i);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3] + i);
__m128i d1 = _mm_unpacklo_epi16(r, b);
__m128i d2 = _mm_unpacklo_epi16(g, a);
@@ -66,7 +66,7 @@ void fromPlanarSSE(const void *in, void *out, size_t count)
__m128i r = _mm_loadu_si128(_in[0] + i);
__m128i g = _mm_loadu_si128(_in[1] + i);
__m128i b = _mm_loadu_si128(_in[2] + i);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
if constexpr(ch==4)a = _mm_loadu_si128(_in[3] + i);
__m128i d1 = _mm_unpacklo_epi32(r, b);
__m128i d2 = _mm_unpacklo_epi32(g, a);
@@ -84,15 +84,15 @@ void fromPlanarSSE(const void *in, void *out, size_t count)
switch(sizeof(T))
{
case 1:
for(uint32_t o=0; o<ch; o++)static_cast<uint8_t*>(out)[i + o] = static_cast<const uint8_t*>(in)[i + o + s2];
for(uint32_t o=0; o<ch; o++)static_cast<uint8_t*>(out)[i*4 + o] = static_cast<const uint8_t*>(in)[i + o*s2];
if(ch==3)static_cast<uint8_t*>(out)[i*4 + 3] = 0xff;
break;
case 2:
for(uint32_t o=0; o<ch; o++)static_cast<uint16_t*>(out)[i + o] = static_cast<const uint16_t*>(in)[i + o + s2];
for(uint32_t o=0; o<ch; o++)static_cast<uint16_t*>(out)[i*4 + o] = static_cast<const uint16_t*>(in)[i + o*s2];
if(ch==3)static_cast<uint16_t*>(out)[i*4 + 3] = 0xffff;
break;
case 4:
for(uint32_t o=0; o<ch; o++)static_cast<uint32_t*>(out)[i + o] = static_cast<const uint32_t*>(in)[i + o + s2];
for(uint32_t o=0; o<ch; o++)static_cast<uint32_t*>(out)[i*4 + o] = static_cast<const uint32_t*>(in)[i + o*s2];
if(ch==3)
{
if(!std::numeric_limits<T>::is_integer)static_cast<float*>(out)[i*4 + 3] = 1.0;
+488 -44
View File
@@ -3,34 +3,47 @@
#include <QFileInfo>
#include <QDebug>
#include <QInputDialog>
#include <QJsonValue>
#include <QJSValueIterator>
#include "loadrunable.h"
#include "rawimage.h"
#include "loadrunable.h"
#include "loadimage.h"
#include "batchprocessing.h"
#include <fitsio2.h>
#include "libXISF/libxisf.h"
#include "libxisf.h"
#ifdef PLATESOLVER
#include "solver.h"
#endif // PLATESOLVER
namespace Script
{
ScriptEngine::ScriptEngine(BatchProcessing *parent)
ScriptEngine::ScriptEngine(Database *database, BatchProcessing *parent)
: _jsEngine(new QJSEngine(this))
, _database(new Database(this))
, _database(database)
, _parent(parent)
, _pool(new QThreadPool(this))
{
QJSValue core = _jsEngine->newQObject(this);
_jsEngine->globalObject().setProperty("core", core);
QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject);
QJSValue textFile = _jsEngine->newQMetaObject(&TextFile::staticMetaObject);
_jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject);
_database->init(QLatin1String("scriptengine"));
_jsEngine->globalObject().setProperty("TextFile", textFile);
_semaphore.release(_pool->maxThreadCount());
_pool->setThreadPriority(QThread::LowPriority);
#ifdef PLATESOLVER
_solver = new Solver(this);
#endif // PLATESOLVER
}
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
_scriptPath = scriptPath;
_paths = paths;
if(!arg.isNull())
_jsEngine->globalObject().setProperty("scriptarg", arg);
setPaths(paths);
_outputDir = outputDir + "/";
}
@@ -46,6 +59,9 @@ const QString &ScriptEngine::outputDir() const
void ScriptEngine::interrupt()
{
#ifdef PLATESOLVER
if(_solver)_solver->abort();
#endif
_jsEngine->setInterrupted(true);
}
@@ -61,17 +77,71 @@ void ScriptEngine::log(const QString &message)
void ScriptEngine::mark(File *file)
{
_database->mark(file->absoluteFilePath());
QString path = file->absoluteFilePath();
QMetaObject::invokeMethod(_database, [this, path](){ _database->mark(path); }, Qt::QueuedConnection);
}
void ScriptEngine::unmark(File *file)
{
_database->unmark(file->absoluteFilePath());
QString path = file->absoluteFilePath();
QMetaObject::invokeMethod(_database, [this, path](){ _database->unmark(path); }, Qt::QueuedConnection);
}
bool ScriptEngine::isMarked(const File *file) const
bool ScriptEngine::isMarked(const File *file)
{
return _database->isMarked(file->absoluteFilePath());
bool ret;
QString path = file->absoluteFilePath();
QMetaObject::invokeMethod(_database, [this, path](){ return _database->isMarked(path); }, Qt::BlockingQueuedConnection, &ret);
return ret;
}
QJSValue ScriptEngine::getObjects(double ra, double dec, double distance)
{
QVector<SkyObject> objects;
QMetaObject::invokeMethod(_database, [this, ra, dec, distance](){
return _database->getObjects(ra - distance, ra + distance, dec - distance, dec + distance); }, Qt::BlockingQueuedConnection, &objects);
QJSValue ret = newArray(objects.size());
qint32 i = 0;
for(auto &object : objects)
{
QJSValue jsObj = newObject();
jsObj.setProperty("name", object.name);
jsObj.setProperty("name2", object.name2);
jsObj.setProperty("ra", object.skyPoint.RA());
jsObj.setProperty("dec", object.skyPoint.DEC());
jsObj.setProperty("mag", object.mag);
ret.setProperty(i++, jsObj);
}
return ret;
}
QJSValue ScriptEngine::getObjects(const QJSValue &bounds)
{
QVector<SkyObject> objects;
double minRa = bounds.property("minRA").toNumber();
double maxRa = bounds.property("maxRA").toNumber();
double minDec = bounds.property("minDEC").toNumber();
double maxDec = bounds.property("maxDEC").toNumber();
QMetaObject::invokeMethod(_database, [this, minRa, maxRa, minDec, maxDec](){
return _database->getObjects(minRa, maxRa, minDec, maxDec); }, Qt::BlockingQueuedConnection, &objects);
QJSValue ret = newArray(objects.size());
qint32 i = 0;
for(auto &object : objects)
{
QJSValue jsObj = newObject();
jsObj.setProperty("name", object.name);
jsObj.setProperty("name2", object.name2);
jsObj.setProperty("ra", object.skyPoint.RA());
jsObj.setProperty("dec", object.skyPoint.DEC());
jsObj.setProperty("mag", object.mag);
ret.setProperty(i++, jsObj);
}
return ret;
}
void ScriptEngine::setMaxThread(int maxthread)
@@ -118,6 +188,41 @@ QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, i
return ret;
}
QJSValue ScriptEngine::question(const QString &question, const QStringList &buttons, const QString &title) const
{
QJSValue ret;
QMetaObject::invokeMethod(_parent, "question", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, question), Q_ARG(QStringList, buttons), Q_ARG(QString, title));
return ret;
}
void ScriptEngine::plot(const QJSValue &graph)
{
QVariant graphV = graph.toVariant(QJSValue::ConvertJSObjects);
if(graphV.isValid())
QMetaObject::invokeMethod(_parent, "plot", Qt::QueuedConnection, Q_ARG(QVariant, graphV));
else
logError("Invalid value to be plotted");
}
QJSValue ScriptEngine::openFile(const QString &fileName, const QString &mode)
{
QFileInfo info(fileName);
if(!info.isAbsolute())
info = QFileInfo(outputDir() + fileName);
TextFile *textFile = new TextFile;
if(textFile->open(info.absoluteFilePath(), mode))
{
return _jsEngine->newQObject(textFile);
}
else
{
logError("Failed to open file " + fileName);
delete textFile;
return false;
}
}
bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap &params, bool async)
{
QString path;
@@ -130,9 +235,9 @@ bool ScriptEngine::convert(File *file, QString &outpath, const QString &format,
path = dir.absoluteFilePath(outpath);
QString f = format.toLower();
if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg")
if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg" && f != "tiff")
{
logError("Output format must be one of xisf fits jpg png bmp");
logError("Output format must be one of xisf fits jpg png bmp tiff");
return false;
}
@@ -151,6 +256,160 @@ bool ScriptEngine::convert(File *file, QString &outpath, const QString &format,
return true;
}
#ifdef PLATESOLVER
void ScriptEngine::setSolverProfile(int index)
{
index -= 1;
if(_solver && index >= SSolver::Parameters::DEFAULT && index < SSolver::Parameters::BIG_STARS)
{
_solver->setParameters((SSolver::Parameters::ParametersProfile)index);
}
}
void ScriptEngine::setSolverProfile(const QVariantMap &profile)
{
if(_solver)
{
SSolver::Parameters params = SSolver::Parameters::convertFromMap(profile);
_solver->setParameters(params);
}
}
QJSValue ScriptEngine::getSolverProfile() const
{
if(_solver)
{
QMap<QString, QVariant> params = SSolver::Parameters::convertToMap(_solver->getProfile());
QJSValue ret = _jsEngine->newObject();
for(auto i = params.begin(); i != params.end(); i++)
{
switch(i.value().metaType().id())
{
case QMetaType::Int:
ret.setProperty(i.key(), i.value().toInt());
break;
case QMetaType::Double:
ret.setProperty(i.key(), i.value().toDouble());
break;
case QMetaType::Bool:
ret.setProperty(i.key(), i.value().toBool());
break;
case QMetaType::QString:
ret.setProperty(i.key(), i.value().toString());
break;
default:
qWarning() << "unhandled metatype" << i.key() << i.value();
break;
}
}
return ret;
}
else
{
return QJSValue();
}
}
void ScriptEngine::setStartingSolution(const QJSValue &solution)
{
if(solution.isObject())
{
if(solution.hasProperty("ra") && solution.hasProperty("dec") && solution.property("ra").isNumber() && solution.property("dec").isNumber())
_solver->setSearchPosition(solution.property("ra").toNumber() / 15.0, solution.property("dec").toNumber());
if(solution.hasProperty("pixscale") && solution.property("pixscale").isNumber())
{
double scale = solution.property("pixscale").toNumber();
_solver->setSearchScale(scale * 0.8, scale * 1.2, SSolver::ScaleUnits::ARCSEC_PER_PIX);
}
}
else
{
_solver->clearStartingPositionAndScale();
}
}
QJSValue ScriptEngine::solveImage(File *file, bool updateHeader)
{
QString path = file->absoluteFilePath();
QJSValue ret = newObject();
if(_solver->loadImage(path))
{
if(_solver->solveImage(true))
{
auto solution = _solver->getSolution();
ret.setProperty("fieldWidth", solution.fieldWidth);
ret.setProperty("fieldHeight", solution.fieldHeight);
ret.setProperty("ra", solution.ra);
ret.setProperty("dec", solution.dec);
ret.setProperty("orientation", solution.orientation);
ret.setProperty("pixscale", solution.pixscale);
ret.setProperty("parity", solution.parity == FITSImage::Parity::POSITIVE);
ret.setProperty("raError", solution.raError);
ret.setProperty("decError", solution.decError);
if(updateHeader)
{
QString error;
if(!_solver->updateHeader(error))
logError(error);
}
}
else
{
logError("Failed to plate solve image " + path);
}
}
else
{
logError("Failed to load image " + path);
}
return ret;
}
QJSValue ScriptEngine::extractStars(File *file, bool hfr)
{
QJSValue ret;
QString path = file->absoluteFilePath();
if(_solver->loadImage(path))
{
if(_solver->extractSources(hfr, true))
{
auto stars = _solver->getStars();
ret = newArray(stars.size());
int i = 0;
for(auto &star : stars)
{
QJSValue starj = newObject();
starj.setProperty("x", star.x);
starj.setProperty("y", star.y);
starj.setProperty("mag", star.mag);
starj.setProperty("flux", star.flux);
starj.setProperty("peak", star.peak);
starj.setProperty("HFR", star.HFR);
starj.setProperty("a", star.a);
starj.setProperty("b", star.b);
starj.setProperty("theta", star.theta);
starj.setProperty("ra", star.ra);
starj.setProperty("dec", star.dec);
starj.setProperty("numPixels", star.numPixels);
ret.setProperty(i++, starj);
}
}
else
{
logError("Failed to extract sources from " + path);
}
}
else
{
logError("Failed to load image " + path);
}
return ret;
}
#endif // PLATESOLVER
QJSValue ScriptEngine::newObject()
{
return _jsEngine->newObject();
@@ -161,14 +420,73 @@ QJSValue ScriptEngine::newArray(uint size)
return _jsEngine->newArray(size);
}
void ScriptEngine::run()
QJSValue ScriptEngine::eval(const QString &program)
{
QJSValue jsPaths = _jsEngine->newArray(_paths.size());
for(qsizetype i=0; i<_paths.size(); i++)
jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i].first, _paths[i].second, this)));
QStringList stackTrace;
QJSValue result = _jsEngine->evaluate(program, QString(), 1, &stackTrace);
if(result.isError())
{
QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
error += "\n" + result.property("stack").toString();
emit newMessage(error, true);
}
else if(!result.isUndefined())
{
emit newMessage(result.toString(), false);
}
return result;
}
QStringList ScriptEngine::complete(const QString &line)
{
QStringList complete;
QJSValue globObj = _jsEngine->globalObject();
QRegularExpression reg("[a-zA-Z_][a-zA-Z0-9_]*");
auto match = reg.match(line);
if(match.hasMatch())
{
QString var = match.captured();
if(globObj.hasProperty(var))
{
complete.clear();
QJSValueIterator it(globObj.property(var));
while(it.hasNext())
{
it.next();
if(it.name() != "constructor" && it.name() != "objectNameChanged")
complete.append(var + "." + it.name());
}
}
}
else
{
QJSValueIterator it(globObj);
while(it.hasNext())
{
it.next();
complete.append(it.name());
}
}
return complete;
}
void ScriptEngine::setPaths(const QList<QPair<QString, QString> > &paths)
{
_paths = paths;
QJSValue jsPaths = _jsEngine->newArray(paths.size());
for(qsizetype i=0; i<paths.size(); i++)
jsPaths.setProperty(i, _jsEngine->newQObject(new File(paths[i].first, paths[i].second, this)));
_jsEngine->globalObject().setProperty("files", jsPaths);
}
void ScriptEngine::run()
{
QFile scriptFile(_scriptPath);
if(!scriptFile.open(QIODevice::ReadOnly))
{
@@ -199,16 +517,18 @@ void File::loadFitsKeywords()
{
_fitsKeywordsLoaded = true;
ImageInfoData info;
if(suffix().toLower() == "xisf")
if(isXISF(suffix()))
{
readXISFHeader(_path, info);
}
else if(suffix().toLower() == "fits" || suffix().toLower() == "fit")
else if(isFITS(suffix()))
{
readFITSHeader(_path, info);
}
else return;
_wcs = info.wcs;
for(auto &record : info.fitsHeader)
{
_fitsKeywords.append(record.key);
@@ -350,31 +670,38 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
_fitsKeywordsLoaded = false;
_fitsKeywords.clear();
if(QRegularExpression("fits?", QRegularExpression::CaseInsensitiveOption).match(suffix()).hasMatch())
if(isFITS(suffix()))
{
fitsfile *file;
int status = 0;
fits_open_diskfile(&file, _path.toLocal8Bit().data(), READWRITE, &status);
QString path = makeUNCPath(_path);
fits_open_diskfile(&file, path.toLocal8Bit().data(), READWRITE, &status);
int num = 0;
fits_get_num_hdus(file, &num, &status);
if(status)
{
_engine->newMessage("Failed to open FITS file", true);
if(_engine)_engine->newMessage("Failed to open FITS file", true);
return false;
}
int imgtype;
int naxis;
long naxes[3] = {0};
int type = -1;
std::vector<int> imageIdxs;
for(int i=1; i <= num; i++)
{
fits_movabs_hdu(file, i, IMAGE_HDU, &status);
fits_get_hdu_type(file, &type, &status);
if(type == IMAGE_HDU)
{
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
break;
if(i == num)return false;
if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
}
}
if(modify->_imageIdx >= imageIdxs.size())return false;
fits_movabs_hdu(file, imageIdxs[modify->_imageIdx], &type, &status);
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
for(auto &remove : modify->_remove)
{
@@ -412,7 +739,7 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
break;
}
default:
_engine->newMessage("Unknown type for KEY " + record.key, true);
if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true);
return false;
break;
}
@@ -420,7 +747,7 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
{
char error[100];
fits_get_errstatus(status, error);
_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true);
if(_engine)_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true);
return false;
}
}
@@ -456,7 +783,7 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
break;
}
default:
_engine->newMessage("Unknown type for KEY " + record.key, true);
if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true);
return false;
break;
}
@@ -464,7 +791,7 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
{
char error[100];
fits_get_errstatus(status, error);
_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true);
if(_engine)_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true);
return false;
}
}
@@ -472,31 +799,41 @@ bool File::modifyFITSRecords(const FITSRecordModify *modify)
return status == 0;
}
else if(suffix() == "xisf")
else if(isXISF(suffix()))
{
try
{
LibXISF::XISFModify modifyXISF;
modifyXISF.open(_path.toLocal8Bit().data());
QFileInfo in(_path);
QFileInfo out(_path + "~");
QString in = makeUNCPath(absoluteFilePath());
QString out = in + "~";
modifyXISF.open(in.toLocal8Bit().data());
qDebug() << "modify" << in << out;
for(auto &remove : modify->_remove)
modifyXISF.removeFITSKeyword(0, remove.toStdString());
modifyXISF.removeFITSKeyword(modify->_imageIdx, remove.toStdString());
for(auto &record : modify->_update)
modifyXISF.updateFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()}, true);
modifyXISF.updateFITSKeyword(modify->_imageIdx, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()}, true);
for(auto &record : modify->_add)
modifyXISF.addFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()});
modifyXISF.addFITSKeyword(modify->_imageIdx, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});
modifyXISF.save(out.absoluteFilePath().toLocal8Bit().toStdString());
for(auto &property : modify->_property)
modifyXISF.updateProperty(modify->_imageIdx, property);
modifyXISF.save(out.toLocal8Bit().toStdString());
modifyXISF.close();
std::filesystem::rename(out.filesystemAbsoluteFilePath(), in.filesystemAbsoluteFilePath());
std::filesystem::rename(out.toLocal8Bit().toStdString(), in.toLocal8Bit().toStdString());
return true;
}
catch(std::filesystem::filesystem_error &err)
{
if(_engine)_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
return false;
}
catch(LibXISF::Error &err)
{
_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
if(_engine)_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
return false;
}
}
@@ -560,7 +897,7 @@ QJSValue File::stats()
{
ImageInfoData info;
std::shared_ptr<RawImage> rawImage;
loadImage(_path, info, rawImage);
loadImage(_path, info, rawImage, 0);
rawImage->calcStats();
RawImage::Stats stats = rawImage->imageStats();
_stats = _engine->newObject();
@@ -574,11 +911,47 @@ QJSValue File::stats()
return _stats;
}
ScriptEngineThread::ScriptEngineThread(BatchProcessing *parent) : QObject(parent)
QJSValue File::calculatedBounds()
{
QJSValue ret = _engine->newObject();
loadFitsKeywords();
if(_wcs)
{
double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
_wcs->calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
ret.setProperty("minRA", minRa);
ret.setProperty("maxRA", maxRa);
ret.setProperty("minDEC", minDec);
ret.setProperty("maxDEC", maxDec);
ret.setProperty("crVal1", crVal1);
ret.setProperty("crVal2", crVal2);
}
return ret;
}
#ifdef PLATESOLVER
QJSValue File::solve(bool updateHeader)
{
if(_solution.isUndefined() || updateHeader)
_solution = _engine->solveImage(this, updateHeader);
return _solution;
}
QJSValue File::extractStars(bool hfr)
{
if(_stars.isUndefined())
_stars = _engine->extractStars(this, hfr);
return _stars;
}
#endif // PLATESOLVER
ScriptEngineThread::ScriptEngineThread(Database *database, BatchProcessing *parent) : QObject(parent)
{
_thread = new QThread();
_thread->setObjectName("ScriptEngine");
_engine = new ScriptEngine(parent);
_engine = new ScriptEngine(database, parent);
_engine->moveToThread(_thread);
connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit);
connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage);
@@ -594,9 +967,9 @@ ScriptEngineThread::~ScriptEngineThread()
if(_engine)_engine->interrupt();
}
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
_engine->setParams(scriptPath, paths, outputDir);
_engine->setParams(scriptPath, paths, outputDir, arg);
}
void ScriptEngineThread::start()
@@ -625,4 +998,75 @@ void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, con
_update.append({key.toLatin1(), value, comment.toLatin1()});
}
void FITSRecordModify::updateProperty(const QString &id, const LibXISF::Variant &value)
{
_property.append(LibXISF::Property(id.toStdString(), value));
}
uint32_t FITSRecordModify::imageIndex() const
{
return _imageIdx;
}
void FITSRecordModify::setImageIndex(uint32_t idx)
{
_imageIdx = idx;
}
bool TextFile::open(const QString &path, const QString &mode)
{
_fr.setFileName(path);
QIODevice::OpenMode openMode;
if(mode == "r")
openMode = QIODevice::ReadOnly;
else if(mode == "w")
openMode = QIODevice::WriteOnly;
else if(mode == "a")
openMode = QIODevice::WriteOnly | QIODevice::Append;
else if(mode == "r+")
openMode = QIODevice::ReadWrite | QIODevice::ExistingOnly;
else if(mode == "w+")
openMode = QIODevice::ReadWrite;
else if(mode == "a+")
openMode = QIODevice::ReadWrite | QIODevice::Append;
else
return false;
openMode |= QIODevice::Text;//always open as text
return _fr.open(openMode);
}
void TextFile::write(const QString &data)
{
_fr.write(data.toUtf8());
}
QString TextFile::read(int maxlen)
{
QByteArray data = _fr.read(maxlen);
return data;
}
QString TextFile::readLine()
{
QByteArray data = _fr.readLine();
return QString::fromUtf8(data);
}
QString TextFile::readAll()
{
QByteArray data = _fr.readAll();
return QString::fromUtf8(data);
}
bool TextFile::seek(qint64 offset)
{
return _fr.seek(offset);
}
qint64 TextFile::pos()
{
return _fr.pos();
}
}
+54 -7
View File
@@ -8,9 +8,11 @@
#include <QThreadPool>
#include <QSemaphore>
#include "database.h"
#include "imageinfo.h"
#include "imageinfodata.h"
#include "libxisf.h"
class BatchProcessing;
class Solver;
namespace Script
{
@@ -28,9 +30,10 @@ class ScriptEngine : public QObject
QString _scriptPath;
QString _outputDir;
QList<QPair<QString, QString>> _paths;
Solver *_solver = nullptr;
public:
explicit ScriptEngine(BatchProcessing *parent = nullptr);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
explicit ScriptEngine(Database *database, BatchProcessing *parent = nullptr);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg);
void reportError(const QString &message);
const QString& outputDir() const;
void interrupt();
@@ -38,16 +41,32 @@ public:
Q_INVOKABLE void log(const QString &message);
Q_INVOKABLE void mark(File *file);
Q_INVOKABLE void unmark(File *file);
Q_INVOKABLE bool isMarked(const File *file) const;
Q_INVOKABLE bool isMarked(const File *file);
Q_INVOKABLE QJSValue getObjects(double ra, double dec, double distance);
Q_INVOKABLE QJSValue getObjects(const QJSValue &bounds);
Q_INVOKABLE void setMaxThread(int maxthread);
Q_INVOKABLE void sync();
Q_INVOKABLE QJSValue getString(const QString &label = QString(), const QString &text = QString()) const;
Q_INVOKABLE QJSValue getInt(const QString &label = QString(), int value = 0);
Q_INVOKABLE QJSValue getFloat(const QString &label = QString(), double value = 0, int decimals = 3) const;
Q_INVOKABLE QJSValue getItem(const QStringList &items, const QString &label = "", int current = 0) const;
Q_INVOKABLE QJSValue question(const QString &question, const QStringList &buttons = {"ok"}, const QString &title = "") const;
Q_INVOKABLE void plot(const QJSValue &pointsArray);
Q_INVOKABLE QJSValue openFile(const QString &fileName, const QString &mode = "r");
bool convert(File *file, QString &outpath, const QString &format, const QVariantMap &params, bool async);
#ifdef PLATESOLVER
Q_INVOKABLE void setSolverProfile(int index);
Q_INVOKABLE void setSolverProfile(const QVariantMap &profile);
Q_INVOKABLE QJSValue getSolverProfile() const;
Q_INVOKABLE void setStartingSolution(const QJSValue &solution = QJSValue());
QJSValue solveImage(File *file, bool updateHeader);
QJSValue extractStars(File *file, bool hfr);
#endif // PLATESOLVER
QJSValue newObject();
QJSValue newArray(uint size);
QJSValue eval(const QString &program);
QStringList complete(const QString &line);
void setPaths(const QList<QPair<QString, QString>> &paths);
public slots:
void run();
signals:
@@ -61,9 +80,9 @@ class ScriptEngineThread : public QObject
QThread *_thread;
ScriptEngine *_engine;
public:
ScriptEngineThread(BatchProcessing *parent = nullptr);
ScriptEngineThread(Database *database, BatchProcessing *parent = nullptr);
~ScriptEngineThread();
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg);
void start();
void interrupt();
signals:
@@ -76,16 +95,19 @@ class FITSRecordModify;
class File : public QObject
{
Q_OBJECT
ScriptEngine *_engine;
ScriptEngine *_engine = nullptr;
QString _path;
QString _root;
QFileInfo _info;
bool _fitsKeywordsLoaded = false;
QStringList _fitsKeywords;
QMultiHash<QString, FITSRecord> _fitsRecords;
std::shared_ptr<WCSDataT> _wcs;
void loadFitsKeywords();
bool mkpath(const QString &path) const;
QJSValue _stats;
QJSValue _solution;
QJSValue _stars;
public:
explicit File(const QString &path, ScriptEngine *engine);
explicit File(const QString &path, const QString &root, ScriptEngine *engine);
@@ -109,6 +131,11 @@ public:
Q_INVOKABLE File* convert(const QString &outpath, const QString &format, const QVariantMap &params = QVariantMap());
Q_INVOKABLE File* convertAsync(const QString &outpath, const QString &format, const QVariantMap &params = QVariantMap());
Q_INVOKABLE QJSValue stats();
Q_INVOKABLE QJSValue calculatedBounds();
#ifdef PLATESOLVER
Q_INVOKABLE QJSValue solve(bool updateHeader = false);
Q_INVOKABLE QJSValue extractStars(bool hfr);
#endif // PLATESOLVER
};
class FITSRecordModify : public QObject
@@ -117,6 +144,8 @@ class FITSRecordModify : public QObject
QStringList _remove;
QVector<FITSRecord> _update;
QVector<FITSRecord> _add;
QVector<LibXISF::Property> _property;
uint32_t _imageIdx = 0;
friend class File;
public:
@@ -124,6 +153,24 @@ public:
Q_INVOKABLE void removeKeyword(const QString &key);
Q_INVOKABLE void updateKeyword(const QString &key, const QVariant &value, const QString &comment = QString());
Q_INVOKABLE void addKeyword(const QString &key, const QVariant &value, const QString &comment = QString());
Q_PROPERTY(uint32_t imageIndex READ imageIndex WRITE setImageIndex);
void updateProperty(const QString &id, const LibXISF::Variant &value);
uint32_t imageIndex() const;
void setImageIndex(uint32_t idx);
};
class TextFile : public QObject
{
Q_OBJECT
QFile _fr;
public:
bool open(const QString &path, const QString &mode);
Q_INVOKABLE void write(const QString &data);
Q_INVOKABLE QString read(int maxlen);
Q_INVOKABLE QString readLine();
Q_INVOKABLE QString readAll();
Q_INVOKABLE bool seek(qint64 offset);
Q_INVOKABLE qint64 pos();
};
}
@@ -4,11 +4,21 @@
#include <QLabel>
#include <QSettings>
#include <QApplication>
#include <QProcess>
#include <QCoreApplication>
#include <QFileInfo>
#include <QMessageBox>
#include <QDir>
#include <QPushButton>
#include <QLineEdit>
#include <QColorDialog>
#include "rawimage.h"
extern int DEFAULT_WIDTH;
extern double SATURATION;
extern int FILTERING;
extern bool BESTFIT;
extern QMap<QString, QColor> headerHighlight;
class EvenNumber : public QSpinBox
{
@@ -75,13 +85,88 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
m_qualityThumbnail->setChecked(QUALITY_RESIZE);
m_qualityThumbnail->setToolTip(tr("Use box filter when downsampling thumbnails instead of nearest. Slightly slower."));
m_bestFit = new QCheckBox(tr("Best Fit on image load"));
m_bestFit->setToolTip(tr("Set Best Fit zoom level when opening new image."));
m_bestFit->setChecked(BESTFIT);
m_headerHighlight = new QListWidget(this);
m_headerHighlight->setToolTip(tr("List of FITS keywords that will be highlighted in Image info"));
for(auto i = headerHighlight.begin(); i != headerHighlight.end(); i++)
{
QListWidgetItem *item = new QListWidgetItem(m_headerHighlight);
item->setText(i.key());
item->setBackground(i.value());
}
m_keyword = new QLineEdit(this);
m_keyword->setPlaceholderText(tr("FITS keyword"));
QPushButton *color = new QPushButton(this);
QPixmap pix(16, 16);
pix.fill(m_color);
color->setIcon(pix);
connect(color, &QPushButton::clicked, [this, color](){
QColor rgb = QColorDialog::getColor(m_color, this);
if(rgb.isValid())
{
QPixmap pix(16, 16);
pix.fill(rgb);
color->setIcon(pix);
m_color = rgb;
}
});
QPushButton *add = new QPushButton(tr("Add keyword highlight"), this);
connect(add, &QPushButton::clicked, [this](){
auto list = m_headerHighlight->findItems(m_keyword->text(), Qt::MatchFixedString | Qt::MatchCaseSensitive);
if(list.size())return;
QListWidgetItem *item = new QListWidgetItem(m_headerHighlight);
item->setText(m_keyword->text());
item->setBackground(m_color);
});
QPushButton *remove = new QPushButton(tr("Remove keyword highlight"), this);
connect(remove, &QPushButton::clicked, [this](){
auto list = m_headerHighlight->selectedItems();
for(auto item : list)
delete item;
});
m_lang = new QComboBox(this);
m_lang->addItems({"English", "Français", "Slovenčina", "Português"});
QString lang;
switch(QLocale().language())
{
default:
case QLocale::English: lang = "en"; break;
case QLocale::French: lang = "fr"; break;
case QLocale::Slovak: lang = "sk"; break;
case QLocale::Portuguese: lang = "pt_BR"; break;
}
lang = settings.value("settings/lang", lang).toString();
if(lang == "en")m_lang->setCurrentIndex(0);
else if(lang == "fr")m_lang->setCurrentIndex(1);
else if(lang == "sk")m_lang->setCurrentIndex(2);
else if(lang == "pt_BR")m_lang->setCurrentIndex(3);
layout->addRow(tr("Image preload count"), m_preloadImages);
layout->addRow(tr("Thumbnails size"), m_thumSize);
layout->addRow(tr("Saturation"), m_saturation);
layout->addRow(tr("Slideshow interval"), m_slideShowTime);
layout->addRow(tr("Image interpolation"), m_filtering);
layout->addRow(tr("Language"), m_lang);
layout->addRow(m_qualityThumbnail);
layout->addRow(m_useNativeDialog);
layout->addRow(m_bestFit);
layout->addRow(new QLabel(tr("FITS header highlight"), this));
layout->addRow(m_headerHighlight);
layout->addRow(m_keyword, color);
layout->addRow(add, remove);
#ifdef Q_OS_WIN64
QPushButton *installThumbnailer = new QPushButton(tr("Install"), this);
installThumbnailer->setToolTip(tr("This will install thumnail generation for FITS and XISF files in File Explorer"));
connect(installThumbnailer, &QPushButton::clicked, this, &SettingsDialog::installThumbnailer);
layout->addRow(tr("Install thumbnailer"), installThumbnailer);
#endif
//layout->addRow(new QLabel(tr("Changes in settings will take effect after program restart.")));
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
@@ -103,6 +188,12 @@ void SettingsDialog::loadSettings()
SATURATION = settings.value("settings/saturation", 95.0).toDouble() / 100.0;
FILTERING = settings.value("settings/filtering", FILTERING).toInt();
QUALITY_RESIZE = settings.value("settings/qualitythumbnail", QUALITY_RESIZE).toBool();
BESTFIT = settings.value("settings/bestfit", BESTFIT).toBool();
QStringList keywords = settings.value("settings/headerhighlightkeywords").toStringList();
QStringList colors = settings.value("settings/headerhighlightcolors").toStringList();
for(int i = 0; i < std::min(keywords.size(), colors.size()); i++)
headerHighlight.insert(keywords[i], QColor::fromString(colors[i]));
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, settings.value("settings/dontusenativedialogs", false).toBool());
}
@@ -116,6 +207,25 @@ bool SettingsDialog::loadThumbsizes()
return OLD_THUMB_SIZE != THUMB_SIZE;
}
void SettingsDialog::installThumbnailer()
{
#ifdef Q_OS_WIN64
QString path = QCoreApplication::instance()->applicationDirPath() + "/tenmonthumbnailer.dll";
if(!QFileInfo::exists(path))
{
QMessageBox::critical(this, tr("Missing dll"), tr("Can't find ") + path);
return;
}
QProcess regsvr;
int ret = regsvr.execute("regsvr32.exe", {"/s", path});
if(ret == 0)
QMessageBox::information(this, tr("Thumbnail support"), tr("Thumbnail generation support sucessufully installed."));
else
QMessageBox::critical(this, tr("Error"), tr("Failed to register thumbnailer. %1").arg(ret));
#endif
}
void SettingsDialog::saveSettings()
{
QSettings settings;
@@ -127,9 +237,32 @@ void SettingsDialog::saveSettings()
settings.setValue("settings/qualitythumbnail", m_qualityThumbnail->isChecked());
QUALITY_RESIZE = m_qualityThumbnail->isChecked();
FILTERING = m_filtering->currentIndex();
BESTFIT = m_bestFit->isChecked();
settings.setValue("settings/filtering", FILTERING);
settings.setValue("settings/bestfit", BESTFIT);
SATURATION = m_saturation->value() / 100.0;
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, m_useNativeDialog->isChecked());
if(DEFAULT_WIDTH != m_preloadImages->value())
emit preloadChanged(m_preloadImages->value());
headerHighlight.clear();
QStringList colors;
for(int i = 0; i < m_headerHighlight->count(); i++)
{
auto item = m_headerHighlight->item(i);
colors.push_back(item->background().color().name());
headerHighlight[item->text()] = item->background().color();
}
settings.setValue("settings/headerhighlightkeywords", headerHighlight.keys());
settings.setValue("settings/headerhighlightcolors", colors);
QString lang;
int langIdx = m_lang->currentIndex();
switch(langIdx)
{
case 0: lang = "en"; break;
case 1: lang = "fr"; break;
case 2: lang = "sk"; break;
case 3: lang = "pt_BR"; break;
}
settings.setValue("settings/lang", lang);
}
@@ -5,6 +5,7 @@
#include <QSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QListWidget>
class SettingsDialog : public QDialog
{
@@ -13,6 +14,8 @@ public:
explicit SettingsDialog(QWidget *parent = nullptr);
static void loadSettings();
static bool loadThumbsizes();
public slots:
void installThumbnailer();
signals:
void preloadChanged(int witdth);
private:
@@ -25,6 +28,11 @@ private:
QCheckBox *m_useNativeDialog;
QCheckBox *m_qualityThumbnail;
QComboBox *m_filtering;
QCheckBox *m_bestFit;
QListWidget *m_headerHighlight;
QColor m_color = Qt::yellow;
QLineEdit *m_keyword;
QComboBox *m_lang;
};
#endif // SETTINGSDIALOG_H
+268
View File
@@ -0,0 +1,268 @@
#include "solver.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <fitsio.h>
#include <QStandardPaths>
#include <QSettings>
#include <wcslib/wcshdr.h>
#include <wcslib/wcsutil.h>
#include "rawimage.h"
#include "loadimage.h"
#include "scriptengine.h"
Solver::Solver(QObject *parent) : QObject(parent)
{
_solver = new StellarSolver(this);
connect(_solver, &StellarSolver::logOutput, this, &Solver::logOutput);
_solver->setProperty("ProcessType", SSolver::SOLVE);
QSettings settings;
setIndexFolder(settings.value("platesolving/indexPath", Solver::getTenmonIndexPath()).toString());
int profileIdx = settings.value("platesolving/profile", 0).toInt();
auto profiles = _solver->getBuiltInProfiles();
_solver->setParameters(profiles[profileIdx]);
connect(_solver, &StellarSolver::finished, this, &Solver::finished);
}
Solver::~Solver()
{
}
void Solver::setIndexFolder(const QString &indexPath)
{
_solver->setIndexFolderPaths(QStringList(indexPath));
}
bool Solver::loadImage(const QString &path)
{
if(path == _path)return true;
_loaded = false;
std::shared_ptr<RawImage> image;
ImageInfoData info;
if(::loadImage(path, info, image, 0, true))
{
return loadImage(image, path);
}
return false;
}
bool Solver::loadImage(std::shared_ptr<RawImage> &image, const QString &path)
{
_rawImage = image;
if(_rawImage->channels() > 1)
_rawImagePlanar = _rawImage->toPlanar();
else
_rawImagePlanar = _rawImage;
switch(_rawImage->type())
{
case RawImage::UINT8:
_stats.dataType = TBYTE;
break;
case RawImage::UINT16:
_stats.dataType = TUSHORT;
break;
case RawImage::UINT32:
_stats.dataType = TUINT;
break;
case RawImage::FLOAT32:
_stats.dataType = TFLOAT;
break;
case RawImage::FLOAT64:
_stats.dataType = TDOUBLE;
break;
default:
_error = tr("Unsupported image data type");
return false;
break;
}
_stats.bytesPerPixel = _rawImage->typeSize(_rawImagePlanar->type());
_stats.channels = _rawImagePlanar->channels();
_stats.width = _rawImagePlanar->width();
_stats.height = _rawImagePlanar->height();
_stats.samples_per_channel = _stats.width * _stats.height;
_solver->clearSearchPosition();
_solver->clearSearchScale();
_loaded = _solver->loadNewImageBuffer(_stats, (const uint8_t*)_rawImagePlanar->data());
_path = path;
return _loaded;
}
bool Solver::solveImage(bool sync)
{
if(_loaded && !_solver->isRunning())
{
_process = SSolver::ProcessType::SOLVE;
_solver->setProperty("ProcessType", _process);
if(sync)return _solver->solve();
else _solver->start();
return true;
}
return false;
}
bool Solver::extractSources(bool hfr, bool sync)
{
if(_loaded && !_solver->isRunning())
{
_process = hfr ? SSolver::ProcessType::EXTRACT_WITH_HFR : SSolver::ProcessType::EXTRACT;
_solver->setProperty("ProcessType", _process);
if(sync)return _solver->extract(hfr);
else _solver->start();
return true;
}
return false;
}
void Solver::abort()
{
_solver->abort();
}
const FITSImage::Solution& Solver::getSolution() const
{
return _solver->getSolution();
}
const QList<FITSImage::Star>& Solver::getStars() const
{
return _solver->getStarList();
}
double Solver::getHFR() const
{
double hfr = 0.0;
auto stars = getStars();
if(stars.empty())return -1.0;
for(auto &star : stars)
{
hfr += star.HFR;
}
return hfr / stars.size();
}
QString Solver::errorMessage() const
{
return _error;
}
bool Solver::updateHeader(QString &error)
{
if(!_solver->solvingDone())
{
error = tr("Solving is not finished");
return false;
}
FITSImage::Solution solution = getSolution();
double rotationDeg = 360.0 - solution.orientation;
if(rotationDeg > 360)rotationDeg -= 360;
double rotationRad = rotationDeg / 180.0 * M_PI;
double cdeltx = (solution.parity == FITSImage::NEGATIVE ? solution.pixscale : -solution.pixscale) / 3600.0;
double cdelty = solution.pixscale / 3600.0;
Script::File file(_path, nullptr);
Script::FITSRecordModify modify;
modify.removeKeyword("RADECSYS");
modify.updateKeyword("CRPIX1", _stats.width / 2.0, QByteArray("x pixel coordinate of the reference point"));
modify.updateKeyword("CRPIX2", _stats.height / 2.0, QByteArray("y pixel coordinate of the reference point"));
modify.updateKeyword("CDELT1", cdeltx, QByteArray("X pixel size (deg)"));
modify.updateKeyword("CDELT2", cdelty, QByteArray("Y pixel size (deg)"));
modify.updateKeyword("CRVAL1", solution.ra, QByteArray("RA of reference pixel (deg)"));
modify.updateKeyword("CRVAL2", solution.dec, QByteArray("DEC of reference pixel (deg)"));
modify.updateKeyword("CD1_1", std::cos(rotationRad) * cdeltx, QByteArray("CD matrix to convert (x,y) to (RA, DEC)"));
modify.updateKeyword("CD1_2",-std::sin(rotationRad) * cdelty, QByteArray("CD matrix to convert (x,y) to (RA, DEC)"));
modify.updateKeyword("CD2_1", std::sin(rotationRad) * cdeltx, QByteArray("CD matrix to convert (x,y) to (RA, DEC)"));
modify.updateKeyword("CD2_2", std::cos(rotationRad) * cdelty, QByteArray("CD matrix to convert (x,y) to (RA, DEC)"));
modify.updateKeyword("CROTA1", rotationDeg, QByteArray("Image twist X axis (deg)"));
modify.updateKeyword("CROTA2", rotationDeg, QByteArray("Image twist Y axis (deg)"));
modify.updateKeyword("CTYPE1", "RA---TAN", QByteArray("first parameter RA, projection TANgential"));
modify.updateKeyword("CTYPE2", "DEC--TAN", QByteArray("first parameter DEC, projection TANgential"));
modify.updateKeyword("RADESYS", "ICRS", QByteArray("International Celestial Reference System"));
modify.updateKeyword("EQUINOX", 2000, QByteArray("Equinox of coordinates"));
LibXISF::F64Matrix matrix(2, 2);
matrix(0, 0) = std::cos(rotationRad) * cdeltx;
matrix(0, 1) =-std::sin(rotationRad) * cdelty;
matrix(1, 0) = std::sin(rotationRad) * cdeltx;
matrix(1, 1) = std::cos(rotationRad) * cdelty;
modify.updateProperty("PCL:AstrometricSolution:ReferenceCelestialCoordinates", LibXISF::F64Vector({solution.ra, solution.dec}));
modify.updateProperty("PCL:AstrometricSolution:ReferenceImageCoordinates", LibXISF::F64Vector({_stats.width / 2.0, _stats.height / 2.0}));
modify.updateProperty("PCL:AstrometricSolution:LinearTransformationMatrix", LibXISF::F64Matrix(matrix));
modify.updateProperty("PCL:AstrometricSolution:ProjectionSystem", LibXISF::String("Gnomonic"));
modify.updateProperty("PCL:AstrometricSolution:ReferenceNativeCoordinates", LibXISF::F64Vector({0, 90}));
bool ret = file.modifyFITSRecords(&modify);
if(!ret)error = tr("Failed to update file header");
else emit headerUpdated(_path);
return ret;
}
void Solver::setParameters(Parameters::ParametersProfile profile)
{
auto profileParam = _solver->getBuiltInProfiles().at(profile);
profileParam.partition = false;
_solver->setParameters(profileParam);
}
void Solver::setParameters(const Parameters &parameters)
{
auto profile = parameters;
profile.partition = false;
_solver->setParameters(profile);
}
Parameters Solver::getProfile() const
{
return _solver->getCurrentParameters();
}
void Solver::setSearchScale(double fovLow, double fowHigh, SSolver::ScaleUnits units)
{
_solver->setSearchScale(fovLow, fowHigh, units);
}
void Solver::setSearchPosition(double ra, double dec)
{
_solver->setSearchPositionRaDec(ra, dec);
}
void Solver::clearStartingPositionAndScale()
{
_solver->clearSearchPosition();
_solver->clearSearchScale();
}
QStringList Solver::getIndexPaths()
{
QStringList paths = StellarSolver::getDefaultIndexFolderPaths();
paths.prepend(getTenmonIndexPath());
return paths;
}
QString Solver::getTenmonIndexPath()
{
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/astrometry";
}
void Solver::finished()
{
switch(_process)
{
case SSolver::ProcessType::SOLVE:
emit solvingDone();
break;
case SSolver::ProcessType::EXTRACT_WITH_HFR:
case SSolver::ProcessType::EXTRACT:
emit extractionDone();
break;
}
}
+53
View File
@@ -0,0 +1,53 @@
#ifndef SOLVER_H
#define SOLVER_H
#include <stellarsolver.h>
class RawImage;
class Solver : public QObject
{
Q_OBJECT
StellarSolver *_solver;
FITSImage::Statistic _stats;
SSolver::ProcessType _process = SSolver::SOLVE;
bool _loaded = false;
QString _path;
QString _error;
std::shared_ptr<RawImage> _rawImage;
std::shared_ptr<RawImage> _rawImagePlanar;
public:
explicit Solver(QObject *parent = nullptr);
~Solver();
void setIndexFolder(const QString &indexPath);
bool loadImage(const QString &path);
bool loadImage(std::shared_ptr<RawImage> &image, const QString &path);
bool solveImage(bool sync = false);
bool extractSources(bool hfr, bool sync = false);
void abort();
const FITSImage::Solution& getSolution() const;
const QList<FITSImage::Star>& getStars() const;
double getHFR() const;
QString errorMessage() const;
bool updateHeader(QString &error);
void setParameters(SSolver::Parameters::ParametersProfile profile);
void setParameters(const SSolver::Parameters &parameters);
SSolver::Parameters getProfile() const;
void setSearchScale(double fovLow, double fowHigh, ScaleUnits units);
void setSearchPosition(double ra, double dec);
void clearStartingPositionAndScale();
static QStringList getIndexPaths();
static QString getTenmonIndexPath();
public slots:
void finished();
signals:
void solvingDone();
void extractionDone();
void headerUpdated(const QString &path);
void logOutput(const QString &log);
};
#endif // SOLVER_H
View File
View File
+78 -12
View File
@@ -12,7 +12,7 @@ static float clamp(float x)
STFSlider::STFSlider(const QColor &color, QWidget *parent) : QWidget(parent)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumWidth(100);
setMouseTracking(true);
@@ -64,12 +64,51 @@ void STFSlider::setMTFParams(float low, float mid, float high)
update();
}
void STFSlider::orientationChanged(Qt::Orientations orientation)
{
m_orientation = orientation;
if(m_orientation == Qt::Horizontal)
{
if(m_color == Qt::white)
{
setMaximumSize(QWIDGETSIZE_MAX, 16);
setMinimumSize(16, 16);
}
else
{
setMaximumSize(QWIDGETSIZE_MAX, 10);
setMinimumSize(10, 10);
}
}
else
{
if(m_color == Qt::white)
{
setMaximumSize(16, QWIDGETSIZE_MAX);
setMinimumSize(16, 16);
}
else
{
setMaximumSize(10, QWIDGETSIZE_MAX);
setMinimumSize(10, 10);
}
}
}
void STFSlider::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QRect rect = event->rect();
qreal w = rect.width() - 1;
qreal h = rect.height();
if(m_orientation == Qt::Vertical)
{
rect = rect.transposed();
painter.rotate(90);
w = rect.width() - 1;
h = rect.height();
painter.translate(0, -h);
}
QLinearGradient gradient(rect.topLeft(), rect.topRight());
gradient.setColorAt(0, Qt::black);
for(int i=1; i<=32; i++)
@@ -93,6 +132,11 @@ void STFSlider::paintEvent(QPaintEvent *event)
{
painter.setPen(p < m_threshold ? Qt::white : Qt::black);
painter.resetTransform();
if(m_orientation == Qt::Vertical)
{
painter.rotate(90);
painter.translate(0, -h);
}
painter.translate(w*p, 0);
painter.drawPath(tick);
};
@@ -105,15 +149,26 @@ void STFSlider::paintEvent(QPaintEvent *event)
void STFSlider::mouseMoveEvent(QMouseEvent *event)
{
const qreal x = event->position().x();
if(std::abs(m_blackPoint*width() - x) < 5 ||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5 ||
std::abs(m_whitePoint*width() - x) < 5)
setCursor(Qt::SplitHCursor);
qreal x,w;
if(m_orientation == Qt::Horizontal)
{
x = event->position().x();
w = width();
}
else
{
x = event->position().y();
w = height();
}
if(std::abs(m_blackPoint*w - x) < 5 ||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*w - x) < 5 ||
std::abs(m_whitePoint*w - x) < 5)
setCursor(m_orientation == Qt::Horizontal ? Qt::SplitHCursor : Qt::SplitVCursor);
else
unsetCursor();
qreal xw = x/width();
qreal xw = x/w;
if(event->modifiers() & Qt::ShiftModifier && !m_fineTune)
{
m_fineTune = true;
@@ -154,18 +209,29 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
void STFSlider::mousePressEvent(QMouseEvent *event)
{
const qreal x = event->position().x();
qreal x,w;
if(m_orientation == Qt::Horizontal)
{
x = event->position().x();
w = width();
}
else
{
x = event->position().y();
w = height();
}
if(event->modifiers() & Qt::ShiftModifier)
{
m_fineTune = true;
m_fineTuneX = x/width();
m_fineTuneX = x/w;
}
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5)
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*w - x) < 5)
m_grabbed = 1;
else if(std::abs(m_blackPoint*width() - x) < 5)
else if(std::abs(m_blackPoint*w - x) < 5)
m_grabbed = 0;
else if(std::abs(m_whitePoint*width() - x) < 5)
else if(std::abs(m_whitePoint*w - x) < 5)
m_grabbed = 2;
else
m_grabbed = -1;
+3
View File
@@ -15,12 +15,15 @@ class STFSlider : public QWidget
float m_fineTuneX;
QColor m_color;
float m_threshold;
Qt::Orientations m_orientation = Qt::Horizontal;
public:
explicit STFSlider(const QColor &color = Qt::white, QWidget *parent = nullptr);
float blackPoint() const;
float midPoint() const;
float whitePoint() const;
void setMTFParams(float low, float mid, float high);
public slots:
void orientationChanged(Qt::Orientations orientation);
signals:
void paramChanged(float blackPoint, float midPoint, float whitePoint);
protected:
+34 -61
View File
@@ -2,19 +2,10 @@
#include <QVBoxLayout>
#include <QDebug>
#include <QToolButton>
#include <QSettings>
#include <QStyle>
#include "imageringlist.h"
const float BLACK_POINT_SIGMA = -2.8f;
const float MAD_TO_SIGMA = 1.4826f;
const float TARGET_BACKGROUND = 0.25f;
float MTF(float x, float m)
{
if(x < 0)return 0;
if(x > 1)return 1;
return ((m - 1) * x) / ((2 * m - 1) * x - m);
}
StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar"), parent)
{
setObjectName("stretchtoolbar");
@@ -22,16 +13,23 @@ StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar")
QVBoxLayout *vbox1 = new QVBoxLayout(lum);
m_stfSlider = new STFSlider(Qt::white, this);
vbox1->addWidget(m_stfSlider);
connect(this, &StretchToolbar::orientationChanged, m_stfSlider, &STFSlider::orientationChanged);
m_stfSliderR = new STFSlider(Qt::red, this);
m_stfSliderG = new STFSlider(Qt::green, this);
m_stfSliderB = new STFSlider(Qt::blue, this);
QWidget *rgb = new QWidget(this);
QVBoxLayout *vbox2 = new QVBoxLayout(rgb);
vbox2->setSpacing(0);
vbox2->addWidget(m_stfSliderR);
vbox2->addWidget(m_stfSliderG);
vbox2->addWidget(m_stfSliderB);
QBoxLayout *box2 = new QBoxLayout(orientation() == Qt::Horizontal ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight, rgb);
box2->setSpacing(0);
box2->addWidget(m_stfSliderR);
box2->addWidget(m_stfSliderG);
box2->addWidget(m_stfSliderB);
connect(this, &StretchToolbar::orientationChanged, m_stfSliderR, &STFSlider::orientationChanged);
connect(this, &StretchToolbar::orientationChanged, m_stfSliderG, &STFSlider::orientationChanged);
connect(this, &StretchToolbar::orientationChanged, m_stfSliderB, &STFSlider::orientationChanged);
connect(this, &StretchToolbar::orientationChanged, [box2](Qt::Orientations orientation){
box2->setDirection(orientation == Qt::Horizontal ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
});
m_stack = new QStackedWidget(this);
m_stack->addWidget(lum);
@@ -91,60 +89,35 @@ StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar")
m_autoStretchOnLoad = addAction(QIcon(":/nuke_a.png"), tr("Apply auto stretch on load"));
m_autoStretchOnLoad->setCheckable(true);
QAction *showGridButton = addAction(QIcon(":/grid.svg"), tr("Draw equatorial grid"));
showGridButton->setCheckable(true);
connect(showGridButton, &QAction::toggled, this, &StretchToolbar::drawGrid);
QSettings settings;
m_autoStretchOnLoad->setChecked(settings.value("stretchtoolbar/autostretch", false).toBool());
}
StretchToolbar::~StretchToolbar()
{
QSettings settings;
settings.setValue("stretchtoolbar/autostretch", m_autoStretchOnLoad->isChecked());
}
const MTFParam &StretchToolbar::params() const
{
return m_mtfParam;
}
void StretchToolbar::stretchImage(Image *img)
{
if(img && img->rawImage())
{
const RawImage::Stats &stats = img->rawImage()->imageStats();
int i = 0;
int ch = 1;
int o = 0;
if(m_stack->currentIndex() == 1 && img->rawImage()->channels() == 1 && m_debayer->isChecked())
{
i = 1;
ch = 4;
o = 1;
}
if(img->rawImage()->channels() >= 3)
ch = 3;
m_mtfParam = img->rawImage()->calcMTFParams(m_stack->currentIndex() == 0,
m_stack->currentIndex() == 1 && img->rawImage()->channels() == 1 && m_debayer->isChecked());
float bp2 = 0;
float mid2 = 0;
float max2 = 0;
for(; i < ch; i++)
{
double median, mad, max;
median = stats.m_median[i];
mad = stats.m_mad[i];
max = stats.m_max[i];
median /= img->rawImage()->norm();
bool a = median > 0.5 ? true : false;
mad /= img->rawImage()->norm();
max = 1.0f;// /= img->rawImage()->norm();
float bp = a || mad == 0.0f ? 0.0f : std::clamp(median + mad * BLACK_POINT_SIGMA * MAD_TO_SIGMA, 0.0, 1.0);
if(a && mad != 0.0f)
max = std::clamp(median - mad * BLACK_POINT_SIGMA * MAD_TO_SIGMA, 0.0, 1.0);
float mid = !a ? MTF(median - bp, TARGET_BACKGROUND) : MTF(TARGET_BACKGROUND, max - median);
m_mtfParam.blackPoint[i-o] = bp;
m_mtfParam.midPoint[i-o] = mid;// / max;
m_mtfParam.whitePoint[i-o] = max;
bp2 += bp;
mid2 += mid;
max2 = max > max2 ? max : max2;
}
if(ch == 1)
{
m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = m_mtfParam.blackPoint[0];
m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = m_mtfParam.midPoint[0];
m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = m_mtfParam.whitePoint[0];
}
if(m_stack->currentIndex() == 0)
{
m_mtfParam.blackPoint[0] = m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = bp2 / ch;
m_mtfParam.midPoint[0] = m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = mid2 / ch;
m_mtfParam.whitePoint[0] = m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = max2;
m_stfSlider->setMTFParams(m_mtfParam.blackPoint[0], m_mtfParam.midPoint[0], m_mtfParam.whitePoint[0]);
}
else

Some files were not shown because too many files have changed in this diff Show More