Compare commits

..

442 Commits

Author SHA1 Message Date
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
nou 92345f82ca Update metainfo 2024-06-10 22:39:52 +02:00
nou 3c8f49e932 Fix some bugs in scripting 2024-06-10 18:55:13 +02:00
nou 37dd97e361 Fix compilation error 2024-06-10 16:46:21 +02:00
nou da31187aa3 Fix small typo in help 2024-06-09 17:15:53 +02:00
nou 4801338160 Fix compilation error 2024-06-09 15:47:30 +02:00
nou fb9d026ff5 Open script folder on MacOS 2024-06-09 15:42:31 +02:00
nou c2810faf8f Update french translation 2024-06-09 13:56:04 +02:00
nou d3d302fd38 Fix some typos in help 2024-06-08 23:02:13 +02:00
nou a0497c7d19 Update translations 2024-06-08 21:19:14 +02:00
nou 8818e25eda Help documentation for batch processing 2024-06-08 20:21:46 +02:00
nou c3baa18087 Fix text color in log 2024-06-08 20:19:48 +02:00
nou 66f0c05a48 Fix calling GUI methods from script thread 2024-06-08 20:11:25 +02:00
nou 461ffea874 Default params for convert() 2024-06-08 19:42:21 +02:00
nou 7535ad87e7 Batchprocessing improvments 2024-06-06 12:00:00 +02:00
nou 273aef1594 Add getInt getString getFloat methods to scripting 2024-06-05 22:27:35 +02:00
nou 9519c9830c Improve text coloring in log 2024-06-04 16:41:50 +02:00
nou 342e5cc5db Add FITSRecordModify for XISF files 2024-06-04 16:41:25 +02:00
nou ae84cbdfe0 Add modifing FITS records 2024-04-12 09:58:21 +02:00
nou 933fd4a2a0 Additional work on batch processing 2024-03-29 18:08:57 +01:00
nou c3588e1c36 Rate limit conversion from script 2024-03-26 14:54:19 +01:00
nou 174134a9ee Skip dummy HDU in compressed FITS 2024-03-25 22:53:51 +01:00
nou bbc13ec8a5 Add compression parameters 2024-03-25 22:53:13 +01:00
nou 9f7e2ab6b4 Add convert function to script 2024-03-25 20:25:47 +01:00
nou 4fe56acbd9 Fix bug when saving color FITS/XISF files 2024-03-24 23:55:50 +01:00
nou f35db9d1af Small fixes 2024-03-24 18:39:46 +01:00
nou 81d138f799 Add bayer mask icons 2024-02-12 17:57:35 +01:00
nou ae07d4793b Draw only visible filenames in thumbnails 2024-02-11 13:52:52 +01:00
nou dc2a781d3b Add calculating stats with script 2024-02-04 00:11:31 +01:00
nou 90035f44ed Refractor LoadRunable 2024-02-04 00:09:46 +01:00
nou 53c9a58125 Prevent symlink loop when indexing 2024-02-03 15:32:34 +01:00
nou 3f7e3689e8 Prevent symlink loops 2024-02-02 22:41:44 +01:00
nou af9187737f Add recursive directory 2024-02-02 20:55:58 +01:00
nou 4e952873e3 Fix metainfo 2024-02-02 00:09:42 +01:00
nou fb24800050 Add DBus for MacOS to fix build issue 2024-02-01 23:27:40 +01:00
nou ea0dcc226a Update metainfo 2024-02-01 23:26:08 +01:00
nou 6a7b677b95 Translatiotions 2024-02-01 23:22:35 +01:00
nou 0cee4c9c53 Add thumbnail quality to settings 2024-02-01 23:03:21 +01:00
nou d5f2351905 Only degress should show negative sign 2024-01-22 21:32:15 +01:00
nou 18732a8cbf Limit image info to 1024 characters 2024-01-22 21:31:28 +01:00
nou 8c9c1d8d06 Flip image according to ROWORDER 2024-01-22 21:30:39 +01:00
nou e5f425ff8d Fix some edge cases when stretch 2024-01-18 16:10:11 +01:00
nou 428f9c360a Small optimization in resample 2024-01-15 08:58:14 +01:00
nou a8a1509db7 Show error message in main window when image fail to load 2024-01-14 14:32:01 +01:00
nou 6539c78c57 Add box resize algorithm 2024-01-14 14:28:28 +01:00
nou 0e0d29320e Set unlimited image size so it doesn't fail to load big images 2024-01-14 14:04:54 +01:00
nou 1efe8e6974 Fix date in meta info 2024-01-11 13:30:30 +01:00
nou dae10182d1 Fix SSE instricts ifdef 2024-01-09 15:40:27 +01:00
nou ed5fc9c1c2 Increase number max preload images to 32 2024-01-08 16:52:46 +01:00
nou cd6a64a98b Additional work on batch processing 2024-01-08 15:44:05 +01:00
nou 67355a82b7 Add bayer mask selection 2024-01-08 15:43:21 +01:00
nou 8fc2078a3a Don't skip images before they load 2024-01-05 13:37:13 +01:00
nou da9b389409 Add slideshow 2024-01-05 13:36:06 +01:00
nou 7818b8d3e9 Fix icon instalation 2023-12-31 16:04:32 +01:00
nou 11294bfcb0 Scripting module 2023-12-31 16:04:16 +01:00
nou faecb385aa Reorganize resources 2023-12-22 11:20:03 +01:00
nou e5be04926b Fix warnings 2023-12-20 11:31:55 +01:00
nou eaf2c7094b Migrate to Qt6 2023-12-20 11:31:51 +01:00
nou aef41f5f6b Fix Qt deprectation warnings 2023-12-18 16:03:04 +01:00
nou 2134f13b06 Add nearest and bicubic filtering 2023-12-18 15:54:15 +01:00
nou 0e9c980325 Add support for CR3 files 2023-11-25 18:06:38 +01:00
nou b9bf6bf183 Fixed scaling for int32 2023-11-21 18:31:07 +01:00
nou 50c070b169 Fix stack size problem on MacOS 2023-11-16 22:13:32 +01:00
nou cfee287bfa Update metainfo 2023-11-16 22:13:25 +01:00
nou 61e0c542f5 Update translations and help 2023-11-16 19:11:36 +01:00
nou a42abb05ea Add scaling for float images that are out of 0.0-1.0 range 2023-11-15 10:14:25 +01:00
nou 5c6df4a59f Optimize calculating stast for debayer 2023-11-15 10:13:49 +01:00
nou 35d5934227 Better stretch non full range images like CR2 2023-11-15 10:12:55 +01:00
nou 8e3c1b35db Remove dead code 2023-11-15 10:02:53 +01:00
nou 544e4abf92 Made unlinked stretch white balancing 2023-11-14 23:47:47 +01:00
nou e97e10fb5b Make STF slider wider 2023-11-14 16:30:57 +01:00
nou 2608a1bc79 Add tooltip with filenames 2023-11-14 16:30:38 +01:00
nou fa69f17e51 Support for unlinked stretch 2023-11-14 12:08:28 +01:00
nou 4a9d720343 Sum all channels histograms 2023-11-14 12:04:55 +01:00
nou d462ece7c9 Improve histogram for 8 bit images 2023-11-13 23:15:34 +01:00
nou 46b0210078 Add histogram 2023-11-13 21:12:23 +01:00
nou 0a803ace10 Prepare for three channels STF 2023-10-10 22:48:40 +02:00
nou 0c2c5f908c Hide fsanitizer behind option 2023-10-10 21:42:26 +02:00
nou c2197298a7 Hide analyze menu 2023-09-30 23:13:11 +02:00
nou e8630330b2 Show stats for each channel 2023-09-30 23:13:02 +02:00
nou 5955a02175 Fix loading RAW files 2023-09-10 22:14:27 +02:00
nou c0b9194ecc Add header files to cmakelists.txt 2023-09-09 17:39:09 +02:00
nou 5f27acbfd1 Add test files 2023-08-29 23:17:16 +02:00
nou f1a2aae9b6 Update LibXISF, fixes in RawImage 2023-08-28 20:38:35 +02:00
nou 9ffbdcee30 Get rid of raw pointers 2023-06-17 21:47:06 +02:00
nou d9b1c253db Move initialization of member variables 2023-06-17 21:45:35 +02:00
nou 7e39304799 Get rid of some explicit new allocation 2023-06-17 20:32:58 +02:00
nou 31cf1ee2b1 Getting rid of opencv 2023-06-16 23:40:11 +02:00
nou ab245f0484 Add false color rendering 2023-06-02 20:41:34 +02:00
nou 77c312800a Update metainfo 2023-04-19 19:07:15 +02:00
nou 21e90b92dc Fix assert lo < hi in std::clamp 2023-04-19 14:33:29 +02:00
nou 2817d3c7c9 Update libXISF 2023-04-12 22:24:45 +02:00
nou a51b0ef02c Floor offset to to prevent half pixel drawing 2023-03-13 22:49:34 +01:00
nou 7b19230366 Fix disrepancy between wheel scroll and bar 2023-03-12 16:44:18 +01:00
nou 26666ee36d Improved zoom and scrolling 2023-03-12 13:45:07 +01:00
nou 74aee15f80 Specify layout index in shader 2023-03-11 13:38:36 +01:00
nou fde1594086 Add file list sorting 2023-03-11 10:41:26 +01:00
nou a9783f6030 Update LibXISF 2023-03-09 18:35:38 +01:00
nou 1ce4f95f53 Release 20230212 2023-02-12 23:02:50 +01:00
nou 1400fd2e32 Fix bug in XISF indexing 2023-02-12 22:59:07 +01:00
nou cc7f56fc53 Change submodule URL 2023-02-10 14:08:37 +01:00
nou 8921ef9c63 Restore accidently removed FITSRecord ctr 2023-02-10 13:45:13 +01:00
nou 576df9c196 Downscale image to max OpenGL texture 2023-02-10 09:11:38 +01:00
nou c47ecbedb8 Replace PCL with LibXISF 2023-02-10 09:10:57 +01:00
nou c7f4e3747a Fix initialization of SATURATION settings 2023-01-12 09:47:41 +01:00
nou 304cd33f34 Fix white balance uninitialized at start 2023-01-11 00:49:26 +01:00
nou abc813ddbb Fix exceptions and errors in analyzing code 2023-01-01 19:04:23 +01:00
nou 88f449d971 Add makeCurrent to all methods that calls OpenGL 2022-12-29 23:12:06 +01:00
nou 295ddb8daf Don't create debug context 2022-12-29 21:56:23 +01:00
nou 1a220bc3ed Update metainfo 2022-12-28 14:59:17 +01:00
nou f67539a3a1 Update translations 2022-12-28 14:51:45 +01:00
nou 468bcb5abb Add saturation statistic 2022-12-28 14:43:47 +01:00
nou fd49ba9a44 CSV export 2022-12-28 13:24:05 +01:00
nou 22e3b06fdd Fix potentional race conditions 2022-12-28 11:33:26 +01:00
nou 01febbf421 Fix invalid query when no column is selected 2022-12-28 11:17:36 +01:00
nou 7690496cf5 Load XISF::ReadImageProperties 2022-12-26 18:07:16 +01:00
nou 743a5f50c4 Disable manual mipmap generation on ATI cards 2022-12-26 10:19:45 +01:00
nou eac534352f Tidy up some debug outputs 2022-12-26 10:19:20 +01:00
nou 57bdc74ef6 Get rid of QRegExp 2022-12-26 10:13:24 +01:00
nou fd1fd7ff08 Get rid of fullscreen mode 2022-12-25 19:34:09 +01:00
nou c1aca3ca65 Add fine tune for STF slider 2022-12-22 12:45:47 +01:00
nou 5cc8fdd83d Simple grey world white balance 2022-12-20 22:27:15 +01:00
nou 66e13529be Allow change of preload without restart 2022-12-18 10:18:01 +01:00
nou e1bed8e1cb Allow change of thumbnail size without restart 2022-12-18 00:17:28 +01:00
nou 151f521688 Set image width and height for XISF 2022-12-17 09:48:35 +01:00
nou 926647e1a7 Don't do unecessary color profile conversion 2022-12-17 09:47:38 +01:00
nou 71fc1f2bd1 Release 20221215 2022-12-15 20:54:39 +01:00
nou f1ff04382b Add option to not use native file dialogs
Thanks to Patrick Chevalley for updated french tranlation
2022-12-15 20:36:19 +01:00
nou ad91adf1d9 Implement workaround for flatpak QFile::moveToTrash 2022-12-15 17:26:16 +01:00
nou b7369c2501 Remove unused variables in debayer shader 2022-12-15 00:25:00 +01:00
nou 79ed6b2059 Fix error in slovak translation 2022-12-15 00:10:52 +01:00
nou 380974a088 Allow saving debayered image 2022-12-15 00:03:29 +01:00
nou b1ad56ca1f Fix debayer shader 2022-12-15 00:03:14 +01:00
nou 58abef5a72 Update metainfo 2022-12-14 22:00:07 +01:00
nou 5427ff57cb Change superpixel tooltip 2022-12-14 21:58:24 +01:00
nou 6a2fa3f656 Update translation 2022-12-14 21:51:19 +01:00
nou c62ec7db8c Add move to trash action 2022-12-14 21:51:05 +01:00
nou d5eb0fdce5 Change key shortcuts 2022-12-14 21:50:30 +01:00
nou 6d25919e1f Add image formats to help 2022-12-14 21:50:05 +01:00
nou 26d1af6077 Add debayer 2022-12-14 21:18:25 +01:00
nou 44d8a8b856 Fix invert button 2022-12-14 17:42:13 +01:00
nou dab6c1f79d Add All files filter to open dialog 2022-12-14 17:16:32 +01:00
nou ce6a4cc40c Refresh dir when show hidden files is toggled 2022-12-13 21:31:23 +01:00
nou 0368c1f1dc Update metainfo xml 2022-12-10 07:48:17 +01:00
nou a1e98d818b Fix copy on btrfs 2022-12-09 19:58:15 +01:00
nou f3f194bcef Update translation and metainfo 2022-12-09 19:33:26 +01:00
nou efd36f96c3 Use native dialogs 2022-12-09 18:38:07 +01:00
nou 37923b37b3 Add error message for copy/move 2022-12-09 18:36:43 +01:00
nou 900453577e Fix includes 2022-11-28 17:36:37 +01:00
nou 34d466c3e0 Show checker pattern with transparent files 2022-11-28 17:32:23 +01:00
nou 9e98127084 Do gamma conversion manualy
Requesting sRGB capable framebuffer is unreliable
2022-11-28 17:31:50 +01:00
nou ba6062b925 Enale loading all image types that Qt can load 2022-11-27 21:10:43 +01:00
nou 6411b7cd15 Release 20221126 2022-11-26 12:03:36 +01:00
nou 223f7cd0ea Refractor save dialog 2022-11-26 11:02:29 +01:00
nou f8f9ee08b3 Add QFileDialog::DontUseNativeDialog 2022-11-22 11:04:04 +01:00
nou af5aed7ef8 Install metainfo generally 2022-11-21 19:09:19 +01:00
nou 8f5249b142 Add metainfo file 2022-11-21 17:44:48 +01:00
nou a7dc942c62 Add scalable icon 2022-11-21 15:49:09 +01:00
nou 1a1399434b Change domain name 2022-11-21 13:18:46 +01:00
nou be567841bf Workaround in AMD OpenGL driver bug
AMD OpenGL driver on Windows doesn't generate mipmaps for sRGB textures
correctly
2022-10-26 23:32:22 +02:00
nou 62d2671112 Update french translation 2022-10-23 12:11:40 +02:00
nou 1f8923512e Update translations 2022-10-23 10:18:21 +02:00
nou 455c3b2d64 Make it combilable with Qt 5.13 and older 2022-10-23 09:54:23 +02:00
nou 4fe546f0e5 Add support for ICC color profiles 2022-10-18 21:19:05 +02:00
nou 95c6fc5343 Enable sRGB to gamma correct scaling 2022-10-16 11:05:15 +02:00
nou 2bc54ea0cc Fix loading RAW on MacOS
LibRaw object is too big for stack so needs to be on heap
2022-10-15 09:56:14 +02:00
nou be6e472081 Settings dialog 2022-10-10 10:30:25 +02:00
nou 9746f8f653 Add option to show hidden files 2022-08-29 18:37:51 +02:00
nou b51a305c63 Remove unused method 2022-08-29 18:29:00 +02:00
nou 39775b5e98 Scale float images to 0,1 range on load 2022-07-22 11:36:10 +02:00
nou 93b56e2966 Workaround for QTBUG-87332 2022-07-03 13:40:54 +02:00
nou 2e41464ff4 Update build instructions 2022-06-27 10:26:27 +02:00
nou b6ae7d4cdb Update french help, fix few typo. Thanks to Patrick Chevalley 2022-06-24 21:14:08 +02:00
nou 1bd48e8fb4 Refres database table when indexing is done 2022-06-24 18:30:28 +02:00
nou 1d65eda490 Search with CRVAL# 2022-06-24 18:06:26 +02:00
nou 97346df596 Update help 2022-06-24 18:05:33 +02:00
nou a157b274a2 Save to database CRVALi 2022-06-23 16:57:00 +02:00
nou 6466702819 Set correct type to vertex attribute 2022-06-22 23:43:56 +02:00
nou b4ea65b42a Upload sizes to OpenGL only once per draw
Should fix MacOS issue
2022-06-22 23:24:29 +02:00
nou 1682de4e1b Update translations 2022-06-22 23:01:35 +02:00
nou 00872b31df Add icon for MacOS 2022-06-21 09:12:26 +02:00
nou 2884787916 Changes to build on MacOS 2022-06-20 23:52:09 +02:00
nou 86ea9fc137 Add MacOS PCL libs 2022-06-20 23:36:31 +02:00
nou 67199a033d Better status bar 2022-06-17 13:24:52 +02:00
nou 0f182900c2 Fix build on older gcc 2022-06-17 09:54:50 +02:00
nou 19ed5ae1a4 Better handling of FITS records 2022-06-17 00:31:27 +02:00
nou 46215c7a7d Workaround for incorect handling of PV1_2 2022-06-16 23:46:54 +02:00
nou c346487504 Add parsing WCS info from XISF 2022-06-16 23:44:28 +02:00
nou 5b6fead6f1 Fix issue with numeric values in XISF FITS header 2022-06-15 08:55:44 +02:00
nou 3e94aa0eda Add search by RA and DEC point 2022-06-14 22:46:40 +02:00
nou 08e70cdb52 Calculate bounds on indexing 2022-06-14 21:44:32 +02:00
nou 04e587b51c Add WCSData::calculateBounds 2022-06-13 23:28:39 +02:00
nou 42dd55244a Add wcslib as lib name 2022-06-13 18:50:57 +02:00
nou 6b9ea5e4b9 Add support for WCS 2022-06-13 18:02:58 +02:00
nou 701a425cc7 Add support for 16 bit PNG images 2022-06-11 22:39:06 +02:00
nou 9cd2ae14b3 Add status bar with color value 2022-06-11 17:19:03 +02:00
nou f7e4e1874f Proper filter setting 2022-06-11 14:10:07 +02:00
nou e6749fc487 Move shaders to subdirectory 2022-06-11 12:27:15 +02:00
nou dc6aa6baa8 Fix bug with wayland backend 2022-06-09 23:30:23 +02:00
nou c8a70d22f8 Show marked files in file list bold 2022-06-04 15:59:51 +02:00
nou dbb533176c Remove unecessary call 2022-06-04 08:10:47 +02:00
nou 032f5b0577 Improve file selection in file system widget 2022-06-03 10:51:51 +02:00
nou eb417010c3 Set max value in autostretch 2022-06-02 15:46:06 +02:00
nou 5b44d2ac69 Add support for NEF, DNG 2022-06-02 15:41:43 +02:00
nou d1df789691 French help and updated translations 2022-05-22 18:49:21 +02:00
nou ab7d04b625 Workaround for unsupported QSqlQuery::size() 2022-05-21 14:53:54 +02:00
nou a4cfc65d4b Wrap reindex into transaction 2022-05-20 22:57:36 +02:00
nou b4746be190 Update help about marking images 2022-05-20 22:41:00 +02:00
nou 9ceb7556f9 Add marking and unmark from thumbnails view 2022-05-20 22:27:53 +02:00
nou 41b29f0701 Show marked files in database view 2022-05-20 12:10:06 +02:00
nou 67ae2d4b62 Mark unmark files from database view 2022-05-20 11:07:18 +02:00
nou b6b6863331 Add override keyword 2022-05-20 10:34:23 +02:00
nou 571fa57af2 Update translations 2022-05-19 09:58:01 +02:00
nou abb3d631bf Double click in file tree open file 2022-05-19 09:51:47 +02:00
nou b65911943e Fix in context menu 2022-05-19 09:47:42 +02:00
nou 9d9f8db499 Fix crash 2022-05-18 17:43:19 +02:00
nou 3060b17c0c Translations update 2022-05-18 17:39:59 +02:00
nou fcf336d63a Second call to QTranslator::load() seem to clear translation 2022-05-18 17:34:15 +02:00
nou 54ef8e990c Selecting thumbnails 2022-05-18 17:33:20 +02:00
nou 94466a6b9b Draw file name under thumbnail 2022-05-09 22:56:06 +02:00
nou b84d8127ad Add copy, move and index action to File tree 2022-05-09 18:14:10 +02:00
nou c971a919ec Add Filetree dock 2022-05-09 15:58:23 +02:00
nou 8c248b7cfc Add French tranlation, credit Patrick Chevalley 2022-05-08 00:18:42 +02:00
nou 43b510a78c Try load translation from application dir 2022-05-08 00:16:11 +02:00
nou 105fba814d Fix drag&drop files on windows 2022-04-27 21:20:31 +02:00
nou 555e6c11c8 Update english help thanks to gunarm 2022-04-27 20:46:59 +02:00
nou 748f5fac03 Update README 2022-04-26 22:40:39 +02:00
nou adc7d07b75 Update help 2022-04-26 22:32:38 +02:00
nou ba450ee554 Error message when OpenGL can't be intialized 2022-04-25 12:00:34 +02:00
nou cc69b7bc2d Fix ifdef 2022-04-25 08:09:29 +02:00
nou 42b619641a Add help dialog 2022-04-24 21:24:55 +02:00
nou 9af4fa1b99 Update translations 2022-04-24 21:24:30 +02:00
nou cbc6775756 Add version to About dialog 2022-04-24 13:30:51 +02:00
nou cee6979ece Reorganize file menu 2022-04-22 22:01:42 +02:00
nou 903ec65d52 Add reindex action 2022-04-22 22:01:21 +02:00
nou 95e4774507 Add translations 2022-04-22 17:40:14 +02:00
nou 2410c51d5d Reformat PCL license 2022-04-22 17:05:25 +02:00
nou 17bca74362 Open external link in About 2022-04-21 23:27:35 +02:00
nou c70123cf7b Prevent crash when changing dir while thumnails are loading 2022-04-21 23:27:19 +02:00
nou 12c6385f77 Add About Dialog and PCL LICENSE 2022-04-21 17:43:24 +02:00
nou 39f3ec7d30 Better tooltip for auto stretch toogle 2022-04-21 16:49:28 +02:00
nou 8b968ddcb1 Add move marked files 2022-04-21 16:47:03 +02:00
nou da1843e48c Add button to autostrech for each file 2022-04-19 19:59:05 +02:00
nou e0d473c8c8 Add marked files dialog 2022-04-19 19:57:06 +02:00
nou 92f9920f24 Add saving to FITS and XISF 2022-04-19 16:57:54 +02:00
nou f68a9c4d7c Right path for flatpak 2022-04-18 15:53:32 +02:00
nou 027a38cb42 Revert install.cmake 2022-04-18 15:46:32 +02:00
nou 47d5a9fc96 Add xisf to mime 2022-04-18 15:40:42 +02:00
nou 061bb3892e Install desktop icon even without xdg-icon-resource 2022-04-18 15:30:46 +02:00
nou b0b1a3a14b Bumb GLSL version to 330 2022-04-18 10:50:19 +02:00
nou ea834ebd16 Change stretchpanel to QToolBar 2022-04-18 10:20:19 +02:00
nou ce836a8ff3 Change fullscreen shortcut 2022-04-18 09:57:06 +02:00
nou a1848b27bf Don't call update for each thumbnail loaded 2022-04-18 09:56:45 +02:00
nou fabf3f0c1a Separate thumnail loading to different pool 2022-04-18 09:55:47 +02:00
nou cba8a0bb9c Clear image sizes buffer to prevent graphical glitches 2022-04-18 09:55:06 +02:00
nou 4e6230eef2 Add thumbnails 2022-04-18 07:20:35 +02:00
nou 2c95364fc4 Reorganize CMakeLists.txt 2022-04-17 16:05:54 +02:00
nou 26be690c70 Add drop file support 2022-04-14 11:20:16 +02:00
nou 56d6db8bc3 Explicitly link gslcblas 2022-04-12 12:20:13 +02:00
nou a44c456cab Add indexing of XISF 2022-04-12 09:03:29 +02:00
nou 0fb27266c7 Support for XISF 2022-04-12 08:17:18 +02:00
nou e5cd25cc77 Multiple search support and SQL optimization 2022-04-11 20:12:06 +02:00
nou c51be18d28 Store UTC mtime not local one 2022-04-11 10:33:53 +02:00
nou 61a618b2fd Don't keep QSqlDatabase instance 2022-04-11 10:33:28 +02:00
nou 84a71896f8 Fix laoding 16 bit signed files 2022-04-10 17:37:10 +02:00
nou 0ba02d4070 Fix calculating median and mad for 32 float images 2022-04-10 16:30:51 +02:00
nou eff0780014 Fix crash if last file in directory is deleted 2022-04-10 16:30:27 +02:00
nou 7665ea76b6 Desktop icon and menu entry when install on Linux 2022-04-10 16:30:10 +02:00
nou 9ba2515aa8 Don't append file column 2022-04-10 12:28:42 +02:00
nou 9b1f165381 Find FITS_INCLUDE on all platforms 2022-04-09 21:48:10 +02:00
177 changed files with 27837 additions and 3369 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "libXISF"]
path = libXISF
url = https://gitea.nouspiro.space/nou/libXISF.git
+126 -29
View File
@@ -2,7 +2,7 @@ cmake_minimum_required (VERSION 3.15)
project(Tenmon)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
@@ -12,42 +12,139 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s")
find_package(Qt5 COMPONENTS Widgets Sql OpenGL REQUIRED)
find_package(OpenCV REQUIRED)
find_library(GSL_LIB gsl REQUIRED)
option(SANITIZE_ADDRESS_LEAK "Enable -fsanitize=address -fsanitize=leak" OFF)
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 Charts REQUIRED)
find_library(EXIF_LIB exif REQUIRED)
find_library(FITS_LIB cfitsio REQUIRED)
find_library(RAW_LIB NAMES raw_r raw REQUIRED)
find_library(RAW_LIB NAMES raw_r 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
database.cpp
databaseview.cpp
filesystemwidget.cpp
imageinfo.cpp
imageringlist.cpp
imagescrollarea.cpp
imagescrollareagl.cpp
loadrunable.cpp
main.cpp
mainwindow.cpp
rawimage.cpp
starfit.cpp
stfslider.cpp
stretchpanel.cpp
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/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
)
qt5_add_resources(TENMON_SRC resources.qrc)
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 icon.rc)
endif(WIN32)
list(APPEND TENMON_SRC resources/icon.rc)
set(tenmon_ICON "")
elseif(APPLE)
set(tenmon_ICON ${CMAKE_CURRENT_SOURCE_DIR}/resources/tenmon.icns)
find_package(Qt6 COMPONENTS DBus REQUIRED)
set_source_files_properties(${tenmon_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
else()
set(tenmon_ICON "")
find_package(Qt6 COMPONENTS DBus REQUIRED)
endif()
add_executable(tenmon ${TENMON_SRC})
qt_add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
if(WIN32)
find_path(FITS_INCLUDE cfitsio/fitsio2.h REQUIRED)
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE}/cfitsio)
endif(WIN32)
find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED)
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR} "src")
target_include_directories(tenmon PRIVATE ${OpenCV_INCLUDE_DIRS})
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 Qt5::Widgets Qt5::Sql Qt5::OpenGL ${OpenCV_LIBS} ${GSL_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB})
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_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 ${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)
endif(APPLE)
if(LIBRAW_STATIC)
add_compile_definitions("LIBRAW_NODLL")
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)
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
if(XDG-DESKTOP-MENU_EXECUTABLE)
install(SCRIPT install.cmake)
else()
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)
option(RELEASE_BUILD "Release build" OFF)
if(RELEASE_BUILD)
add_custom_target(tenmon_version COMMAND ${CMAKE_COMMAND} -Dlocal_dir="${CMAKE_CURRENT_SOURCE_DIR}" -Doutput_dir="${CMAKE_CURRENT_BINARY_DIR}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/gitversion.cmake")
add_dependencies(tenmon tenmon_version)
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)
+28 -6
View File
@@ -1,11 +1,33 @@
Simple image viewer with multithreaded image loading
FITS/XISF image viewer with multithreaded image loading
To get all dependencies install these packages
sudo apt install qtbase5-dev libraw-dev libexif-dev libcfitsio-dev
sudo apt install qt6-base-dev qt6-declarative-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev wcslib-dev cmake libzstd-dev libqt6sql6-sqlite
Then to build run
on OpenSUSE
qmake .
make
./tenmon
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.
homebrew install qt6 libraw cfitsio libexif libgsl wcslib
You may need to set CMAKE_PREFIX_PATH for Qt6 so CMake can find them.
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
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/
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+340
View File
@@ -0,0 +1,340 @@
<!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; }
img { margin: 5px; }
</style>
</head>
<body>
<h2>Tenmon help</h2>
<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>
<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.
The <i>File system</i> panel shows other images in the same directory as the loaded image. At the top is the main menu and below that is the
<i>Stretch panel</i> containing various options for auto-stretching linear image data.</p>
<p>All panels in the interface can be moved around and/or closed. Any closed or non-visible panel can be
re-opened through the <i>Docks</i> menu at the top.</p>
<p>At bottom there is status bar that show current lightness or red, green, blue pixel value under mouse cursor then X and Y coordinates and
if image contain World Coordinate System metadata it show celestial coordinates.</p>
<h3>Opening and saving images</h3>
<p>To load an image select <i>File->Open</i> and choose the file. After a file is loaded, it becomes visible in the main image panel, and the
<i>File system</i> panel will show the other images in the same directory.</p>
<p>The loaded image can be exported to a different format with <i>File->Save as</i>. Any of the formats JPEG, PNG FITS and XISF can be selected.
In the case of saving JPEG or PNG, the stretch function is applied to the saved image. FITS and XISF are saved/converted without applying the stretch.
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>
<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>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" 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>
<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>
<li><i>Reset</i> reset three values for black, mid and white point to default.</li>
<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.</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>
<p>Images can be marked in the <i>Select</i> menu. To show a list of only the marked images, use <i>Select->Show marked</i>.
This dialog can be useful to clear marks from images. Marked images show a <b>*</b> character in the title bar of the main window.
Marked images can be copied or moved to a selected directory with <i>File->Copy/Move marked files</i>.
After copying or moving, the list of marked files is cleared. The list of marked files will be remembered after quitting the program.</p>
<p>Another way to mark images is in database view where you can select rows and then select mark or unmark action in context menu. Marked
files will be shown with bold text. Third way to mark files is from thumbnails view where you can press press <i>Shift</i> and click with left
mouse button and drag across thumbnails to mark them. Holding <i>Ctrl</i> will unmark files.</p>
<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>
<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>
<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>
<p>To populate the database, select a directory of FITS/XISF files with <i>File->Index directory</i>.
After the selected directory is searched, metadata parsed from the images will be stored in the database.
To refresh the database, run <i>File->Reindex</i>. This will update any changed metadata and remove any record of
deleted files. To index new files, simply run <i>Index directory</i> again.</p>
<p>The database is viewed through a panel which is not visible in the default layout. To add the database panel to the view,
toggle <i>Docks->FITS/XISF database</i>. Once visible, database panel shows the database as a table with a column for each property.
Below the table is button to select which columns/properties are displayed.</p>
<p>Also at the bottom of the database panel are three combo boxes and text inputs used for filtering.
Select the property to filter on with the combo box and in the adjacent text box enter a string to search for in that property.
These three combo box contain list of all properties that are found during indexing except first five. First one set searching in file name.
Next two "RA pos" and "DEC pos" allow to filter out indexed images that contain point with entered RA/DEC coordinate. Expected format is three
number separated by space. In case of "DEC pos" it also accept +- sign. Omitting one or two last number is also valid. Some examples "02 12 32" "-12 43 12" "+45 32" "13".
So for RA it means hour, minutes and seconds while for DEC it is degrees, minutes and seconds.
Setting both "RA pos" and "DEC pos" can return images that doesn't contain entered point as it search against minimum and maximum RA/DEC coordinates that images contain.
"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>
<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.</li>
<li>Without wildcard characters, the exact string must match.</li>
</ul>
<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>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>Batch processing</h3>
<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.</p>
<pre>for(file of files)
{
if(file.suffix() == "fits")
{
core.log(file.fileName());
file.convert(file.relativeFilePath(), "XISF");
}
}
</pre>
<p>Bellow this list is output directory. All relative paths passed as arguments into methods like copy(), move() or convert() will be relative to this output directory. If you pass absolute path to methods then
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/&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 is console that enable run simple script commands and
buttons that can start or stop execution of selected scripts.</p>
<h4>core</h4>
<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>
<li><b>unmark(file)</b> unmark file same was as in GUI. Takes object of type <i>File</i> as argument.</li>
<li><b>isMarked(file)</b> check if file was marked. Takes object of type <i>File</i> as argument.</li>
<li><b>setMaxThread(maxthread)</b> set maximum number of concurrent thread when doing asynchronous task.</li>
<li><b>sync()</b> wait until all asynchronous tasks are done.</li>
<li><b>getString(label = "", text = "")</b> show dialog box to get string value from user. String value passed in first argument is used as description label. Second argument text is default value in text box.
Both parameters are optional so calling just <i>getString()</i> is valid. When cancel is pressed it return Undefined.</li>
<li><b>getInt(label = "", value = 0)</b> show dialog box with input box to retrieve integer value. String value passed in first argument is used as description label.
Second parameter is default value in input box. Both parameters are optional. When cancel is pressed it return Undefined.</li>
<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>
<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>
<li><b>absolutePath()</b> returns an absolute path without the file name</li>
<li><b>relativeFilePath()</b> return relative path including file name relative to directory that was in list of directories to be scanned. For example you add C:/images as input directory. In this directory there
is file <i>C:/images/lights/red/M42_001.fits</i> then this method will return <i>lights/red/M42_001.fits</i></li>
<li><b>relativePath()</b> return same path as previous method just without file name. <i>lights/red</i></li>
<li><b>baseName()</b> return file name up to the first dot. For example for <i>some.file.name.fits</i> it will return <i>some</i></li>
<li><b>completeBaseName()</b> return file name up to the last dot. For example for <i>some.file.name.fits</i> it will return <i>some.file.name</i></li>
<li><b>suffix()</b> return string after last dot in file name. For example <i>fits</i></li>
<li><b>size()</b> return size of file in bytes.</li>
<li><b>fitsKeywords()</b> return array of strings with every keyword that is in header. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
<li><b>fitsValue(key)</b> return value for keyword. In case that there is multiple occurrences it return last one.</li>
<li><b>fitsValues(key)</b> return array of values for keyword.</li>
<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
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. 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");
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)
{
if(file.suffix() == "fits")
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
}
core.sync(); // ensure that files exist
for(file of convertedFiles)// now we can iterate over the files
{
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
}</pre></li>
<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>
<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");
modify.removeKeyword("OBJECT");
// doesn't matter that it is specified as last. This will first remove
// existing OBJECT record and then add again OBJECT=M42
for(file in files)
{
file.modifyFITSRecords(modify);
}</pre>
<ul>
<li><b>new FITSRecordModify()</b> create new instance of object.</li>
<li><b>removeKeyword(key);</b> specify removing of record with <i>key</i> as keyword.</li>
<li><b>updateKeyword(key, value, comment = "")</b> specify updating existing keyword with value and comment. Comment is optional parameter. If record with keyword doesn't exist then it will add new one.
Unless you want to have multiple records with same keyword (for example HISTORY) always use this method and not addKeyword.</li>
<li><b>addKeyword(key, value, comment = "")</b> specify adding new keyword</li>
</ul>
<p><small>PS: Kanji in icon means astronomy in Japanese</small></p>
</body>
</html>
+272
View File
@@ -0,0 +1,272 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<head>
<style type="text/css">
h1, h2, h3, h4 { padding:0px; margin:10px; }
p { padding:0px; margin:5px; }
img { margin: 5px; }
</style>
</head>
<body>
<h2>Aide de Tenmon</h2>
<p>Tenmon est destiné principalement à la visualisation de photos et d'images astronomique. Il prend en charge les formats suivants :
<ul>
<li>FITS 8, 16 bit entier et 32 bit point flottant</li>
<li>XISF 8, 16 bit entier et 32 bit point flottant</li>
<li>images JPEG, PNG, BMP, GIF, PBM, PGM, PPM et SVG</li>
<li>images RAW CR2, CR3, NEF, DNG</li>
</ul>
</p>
<h3>Fenêtre principale</h3>
<p>La fenêtre principale affiche l'image actuellement chargée. Sur la gauche se trouve le panneau <i>Informations de l'image</i> qui affiche des détails sur l'image chargée.
Le panneau <i>Système de fichier</i> affiche d'autres images dans le même répertoire que l'image chargée.
En haut se trouve le menu principal et en dessous se trouve le panneau <i>Réglage de la luminosité</i> contenant diverses options pour l'étirement automatique des images linéaires.</p>
<p>Tous les panneaux de l'interface peuvent être déplacés et/ou fermés. Tout panneau fermé ou non visible peut être rouvert via le menu <i>Fenêtres encrables</i> en haut.</p>
<p>En bas, il y a une barre d'état qui affiche l'intensité ou la valeur du pixel rouge, vert, bleu sous le curseur de la souris, puis les coordonnées X et Y et,
si l'image contient des métadonnées du World Coordinate System, elle affiche les coordonnées célestes.</p>
<h3>Ouvrir et enregistrer des images</h3>
<p>Pour charger une image, sélectionnez <i>Fichier->Ouvrir</i> et choisissez le fichier. Une fois qu'un fichier est chargé,
il devient visible dans le panneau d'image et le panneau du <i>système de fichiers</i> affiche les autres images dans le même répertoire.</p>
<p>L'image chargée peut être exportée dans un format différent avec <i>Fichier->Enregistrer sous</i>. Tous les formats JPEG, PNG FITS et XISF
peuvent être sélectionnés. Dans le cas d'un enregistrement JPEG ou PNG, la fonction d'étirement de la luminosité est appliquée à l'image enregistrée.
FITS et XISF sont enregistrés/convertis sans appliquer l'étirement.
Pour ouvrir une image, vous pouvez également la faire glisser et la déposer dans la fenêtre principale.</p>
<h3>Voir</h3>
<p>Le menu <i>Voir</i> propose des options pour contrôler la taille et l'échelle des images affichées :
<ul>
<li><i>Zoom avant</i> et <i>Zoom arrière</i> agrandissent et rétrécissent l'image. La molette de la souris peut également être utilisée pour zoomer librement.</li>
<li><i>Meilleur ajustement</i>, zoom automatiquement l'image pour l'adapter à la taille actuelle de la fenêtre.</li>
<li><i>100 %</i>, zoom à l'échelle 1:1.</li>
<li><i>Plein écran</i> agrandit la fenêtre principale sur tout l'écran.</li>
<li><i>Vignettes</i>, affiche de petites vignettes pour toutes les images du répertoire actuel.</li>
</ul>
<p>
<h3>Barre d'outils du réglage de la luminosité</h3>
<p>Ces outils modifient la luminosité des images affichées.
<br><img src=":/about/stretch-panel.png"></p>
<p>À partir de la gauche, il y a une échelle de luminosité avec trois points réglables pour contrôler manuellement l'étirement.
<ul>
<li>point noir - tous les pixels avec une valeur inférieure (plus sombre) que ce paramètre seront écrêtés en noir</li>
<li>point médian - définit la valeur à étirer à 50 % d'intensité</li>
<li>point blanc - tous les pixels avec une valeur supérieure (plus lumineuse) que celle-ci seront écrêtés en blanc</li>
</ul>
Après le curseur se trouvent 5 boutons pour la luminosité automatique :
<ul>
<li><i>Luminosité automatique</i>, applique automatiquement les points noirs et moyens pour rendre l'image avec une luminosité optimale.</li>
<li><i>Réinitialiser</i>, réinitialise les trois valeurs pour le point noir, moyen et blanc par défaut.</li>
<li><i>Inverser</i>, inverse les couleurs pour afficher l'image en négatif.</li>
<li><i>Super pixel CFA </i>, moyenne 2x2 pixels en un (adapté aux images d'une caméra couleur).</li>
<li><i>Appliquer la luminosité automatique au chargement</i>, applique la luminosité automatique pour chaque image lors du chargement.</p>
</ul>
<h3>Marquer les images</h3>
<p>Les images peuvent être marquées dans le menu <i>Sélectionner</i>. Pour afficher une liste des seules images marquées, utilisez <i>Sélectionner->Afficher marqué</i>. Cette boîte de dialogue peut être utile pour effacer les marques des images. Les images marquées affichent un caractère <b>*</b> dans la barre de titre de la fenêtre principale. Les images marquées peuvent être copiées ou déplacées vers un répertoire sélectionné avec <i>Fichier->Copier/Déplacer les fichiers marqués</i>. Après la copie ou le déplacement, la liste des fichiers marqués est effacée. La liste des fichiers marqués sera mémorisée après avoir quitté le programme.</p>
<p>Une autre façon de marquer des images est dans la vue de la base de données où vous pouvez sélectionner des lignes, puis sélectionner l'action marquer ou décocher dans le menu contextuel. Les fichiers marqués seront affichés en texte gras. La troisième façon de marquer les fichiers est de voir les vignettes où vous pouvez appuyer sur <i>Maj</i> et cliquer avec le bouton gauche de la souris et faire glisser sur les vignettes pour les marquer. Maintenir <i>Ctrl</i> décochera les fichiers.</p>
<h3>Système de fichier et arborescence</h3>
<p>Le panneau du système de fichiers contient la liste des images du répertoire ouvert. Vous pouvez sélectionner un fichier dans cette liste et il sera affiché. Il est également possible
d'utiliser les touches fléchées pour revenir en arrière (gauche et haut) et avancer (droite et bas) entre les images.</p>
<p>L'arborescence des fichiers montre la structure du système de fichiers. Vous pouvez cliquer avec le bouton droit de la souris pour afficher un menu contextuel permettant d'effectuer diverses actions du menu <i>Fichier</i>. Il y a aussi les actions suivantes
<ul>
<li><i>Définir comme répertoire racine</i> afficher uniquement ce répertoire et ses sous-répertoires</li>
<li><i>Réinitialiser la racine</i> afficher tout le système de fichiers</li>
<li><i>Monter</i> afficher le répertoire qui est un niveau au-dessus du répertoire racine actuel</li>
</ul>
</p>
<h3>Base de donnée de fichiers FITS/XISF</h3>
<p>Tenmon peut analyser un répertoire de fichiers FITS/XISF et indexer les métadonnées des en-têtes FITS dans sa base de données interne. Cela permet de rechercher et de trier des images en fonction de ces métadonnées.</p>
<p>Pour remplir la base de données, sélectionnez un répertoire de fichiers FITS/XISF avec <i>Fichier->Indexer le répertoire</i>. Une fois le répertoire parcouru, les métadonnées analysées à partir des images seront stockées dans la base de données. Pour actualiser la base de données, exécutez <i>Fichier-> Ré-indexer les fichiers</i>. Cela mettra à jour toutes les métadonnées modifiées et supprimera tout enregistrement de fichiers supprimés. Pour indexer de nouveaux fichiers, exécutez simplement à nouveau <i>Fichier->Indexer le répertoire</i>.</p>
<p>La base de données est visualisée via un panneau qui n'est pas visible dans la mise en page par défaut. Pour ajouter le panneau de base de données à la vue, basculez <i>Fenêtres encrables->Base de données FITS/XISF</i>. Une fois visible, le panneau de la base de données affiche la base de données sous forme de tableau avec une colonne pour chaque propriété. Sous le tableau se trouve un bouton pour sélectionner les colonnes/propriétés à afficher.</p>
<p>Au bas du panneau de la base de données se trouvent également trois boîtes de liste déroulante et des entrées de texte utilisées pour le filtrage. Sélectionnez la propriété à filtrer dans une liste déroulante et dans la zone de texte adjacente, entrez un texte à rechercher pour cette propriété.
Ces trois boîtes contiennent la liste de toutes les propriétés qui sont trouvées pendant l'indexation, sauf les trois premières. La première définit la recherche dans le nom du fichier.
Les deux suivantes "RA pos" et "DEC pos" permettent de filtrer les images indexées qui contiennent un point avec les coordonnées RA/DEC entrée. Le format attendu est trois nombres séparés par un espace.
Dans le cas de "DEC pos", il accepte également le signe +-. L'omission d'un ou deux derniers chiffres est également valable. Quelques exemples "02 12 32" "-12 43 12" "+45 32" "13".
Le fait de définir à la fois "RA pos" et "DEC pos" peut renvoyer des images qui ne contiennent pas le point saisi, car la recherche est faite sur les coordonnées RA/DEC minimum et maximum que les images contiennent.
"RA range" et "DEC range" filtrent les images dont les coordonnées centrale se trouvent dans la plage saisie.
En appuyant sur la touche Enter ou en cliquant sur le bouton <i>Filtre</i>, les enregistrements de la base de données seront filtrés en fonction des paramètres de recherche.
<p>Caractères génériques :
<ul>
<li><b>%</b> (pourcentage) est un caractère générique représentant zéro ou plusieurs caractères.</li>
<li><b>_</b> (trait de soulignement) est un caractère générique pour exactement un caractère quelconque.</li>
<li>En l'absence de caractères génériques, le texte exacte doit correspondre.</li>
</ul>
</p>
<br><img src=":/about/filter.png"><br>
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>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.
Vous pouvez ajouter des répertoires ou des fichiers individuels à cette liste. Les répertoires sont analysés de manière récursive pour trouver tous les fichiers, même les fichiers non image. Cette liste de fichiers est ensuite transmise au script dans un tableau nommé <b>files</b>.
Dans le script, vous pouvez ensuite parcourir les fichiers comme ici.
<pre>for(file of files)
{
if(file.suffix() == "fits")
{
core.log(file.fileName());
file.convert(file.relativeFilePath(), "XISF");
}
}
</pre>
<h4>core</h4>
Il existe un objet global appelé <b>core</b> qui possède ces méthodes.
<ul>
<li><b>log(message)</b> afficher le message dans la fenêtre du journal.</li>
<li><b>mark(file)</b> marquer le fichier de la même manière que dans l'interface graphique. Prend un objet de type <i>File</i> comme argument.</li>
<li><b>unmark(file)</b> décoche le fichier de la même manière que dans l'interface graphique. Prend un objet de type <i>File</i> comme argument.</li>
<li><b>isMarked(file)</b> vérifie si le fichier a été marqué. Prend un objet de type <i>File</i> comme argument.</li>
<li><b>setMaxThread(maxthread)</b> définir le nombre maximal de threads simultanés lors de l'exécution d'une tâche asynchrone.</li>
<li><b>sync()</b> attendre que toutes les tâches asynchrones soient terminées.</li>
<li><b>getString(label = "", text = "")</b> affiche la boîte de dialogue pour obtenir un text de l'utilisateur. La valeur text passée dans le premier argument est utilisée comme label de description. Le texte du deuxième argument est la valeur par défaut dans la zone de texte.
Les deux paramètres sont facultatifs, donc l'appel à <i>getString()</i> est valide. Lorsque vous appuyez sur Annuler, il renvoie Undefined</li>
<li><b>getInt(label = "", value = 0)</b> affiche une boîte de dialogue avec une zone de saisie pour récupérer une valeur entière. 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 deux paramètres sont facultatifs. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
<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>
Dans le tableau <b>files</b>, il y a des instances d'objets de type <b>File</b> qui ont ces méthodes.
<ul>
<li><b>fileName()</b> renvoie le nom du fichier, à l'exclusion du chemin.</li>
<li><b>absoluteFilePath()</b> renvoie un chemin absolu incluant le nom du fichier.</li>
<li><b>absolutePath()</b> renvoie un chemin absolu sans le nom du fichier</li>
<li><b>relativeFilePath()</b> renvoie le chemin relatif incluant le nom du fichier par rapport au répertoire qui était dans la liste des répertoires à analyser. Par exemple, vous ajoutez C:/images comme répertoire d'entrée. Dans ce répertoire, il y a
le fichier <i>C:/images/lights/red/M42_001.fits</i>, alors cette méthode renverra <i>lights/red/M42_001.fits</i></li>
<li><b>relativePath()</b> renvoie le même chemin que la méthode précédente, mais sans le nom de fichier. <i>lights/red</i></li>
<li><b>baseName()</b> renvoie le nom du fichier jusqu'au premier point. Par exemple, pour <i>some.file.name.fits</i>, il renverra <i>some</i></li>
<li><b>completeBaseName()</b> renvoie le nom du fichier jusqu'au dernier point. Par exemple, pour <i>some.file.name.fits</i>, il renverra <i>some.file.name</i></li>
<li><b>suffix()</b> renvoie la chaîne après le dernier point du nom de fichier. Par exemple <i>fits</i></li>
<li><b>size()</b> renvoie la taille du fichier en octets.</li>
<li><b>fitsKeywords()</b> renvoie un tableau de chaînes avec chaque mot-clé présent dans l'en-tête. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
<li><b>fitsValue(key)</b> renvoie la valeur pour le mot-clé. En cas d'occurrences multiples, la dernière est renvoyée.</li>
<li><b>fitsValues(key)</b> renvoie un tableau de valeurs pour le mot clé.</li>
<li><b>fitsRecords()</b> renvoie un tableau d'objets avec des propriétés <b>key, value</b> et <b>comment</b> </li>
<li><b>modifyFITSRecords(FITSRecordModify)</b> modifier l'en-tête FITS en ajoutant, supprimant ou mettant à jour l'enregistrement FITS. Renvoie true en cas de succès. Reportez-vous à <i>FITSRecordModify</i></li>
<li><b>isMarked()</b> renvoie true si le fichier est marqué.</li>
<li><b>copy(newpath)</b> Copie le fichier vers un nouvel emplacement. Il renvoie une instance du nouvel objet <i>File<i> qui représente ce fichier copié. 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 copié vers ce chemin. En cas d'échec de la copie, il renvoie null.</li>
<li><b>move(newpath)</b> déplacer le fichier vers un nouvel emplacement. Il renvoie false si le déplacement a échoué. Cela peut se produire si la destination n'est pas accessible en écriture mais aussi si le fichier de destination existe déjà. Cette fonction n'écrase pas le fichier existant.
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", "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é.
Il renvoie une nouvelle instance de <i>File</i> qui pointe vers le fichier converti.
<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> même méthode que la précédente, mais effectue la conversion dans un thread séparé de manière asynchrone et en parallèle. Avant d'appeler une méthode sur un objet renvoyé par cette méthode, vous devez appeler
<code>core.sync();</code> pour s'assurer que la conversion est effectuée et que le fichier de destination existe.
<pre>let compression = {"compressionType": "zstd+sh"};
let convertedFiles = [];
for(file of files)
{
if(file.suffix() == "fits")
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
}
core.sync(); // ensure that files exist
for(file of convertedFiles)// now we can iterate over the files
{
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
}</pre></li>
<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>
Cette classe est utilisée pour définir l'en-tête FITS des opérations de modification dans les fichiers FITS et XISF. Elle peut supprimer, mettre à jour et ajouter des enregistrements. L'ordre des opérations est également le suivant : suppression, puis mise à jour et enfin ajout.
Les noms des mots-clés peuvent comporter jusqu'à 8 caractères et ne peuvent contenir que des lettres majuscules, les chiffres de 0 à 9, le trait d'union et le caractère de soulignement.
<pre>let modify = new FITSRecordModify();
modify.updateKeyword("OBJECT", "M42");
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
modify.removeKeyword("OBJECT");
// Peu importe qu'il soit spécifié comme dernier. Cela supprimera d'abord
// l'enregistrement OBJECT existant, puis ajoutera à nouveau OBJECT=M42
for(file in files)
{
file.modifyFITSRecords(modify);
}</pre>
<ul>
<li><b>new FITSRecordModify()</b> créer une nouvelle instance de l'objet.</li>
<li><b>removeKeyword(key);</b> spécifier la suppression de l'enregistrement avec <i>key</i> comme mot-clé.</li>
<li><b>updateKeyword(key, value, comment = "")</b> spécifiez la mise à jour du mot-clé existant avec la valeur et le commentaire. Le commentaire est un paramètre facultatif. Si l'enregistrement avec le mot-clé n'existe pas, il en ajoutera un nouveau.
À moins que vous ne souhaitiez avoir plusieurs enregistrements avec le même mot-clé (par exemple HISTORY), utilisez toujours cette méthode et non addKeyword.</li>
<li><b>addKeyword(key, value, comment = "")</b> spécifier l'ajout d'un nouveau mot-clé</li>
</ul>
<p><small>PS: Le Kanji de icône (tenmon) signifie astronomie en japonais</small></p>
</body>
</html>
+310
View File
@@ -0,0 +1,310 @@
<!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; }
</style>
</head>
<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>
<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>
<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>
<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
tento zobrazený v hlavnom okne. Taktiež sú v panely <i>Súborový systém</i> zobrazené ďalšie obrázky v
adresári kde sa nachádza zobrazený obrázok.</p>
<p>Aktuálne zobrazený obrázok je možné uložiť v inom formáte cez voľbu <i>Súbor->Ulož ako</i>. Dá sa vybrať
formát JPEG, PNG, FITS a XISF. V prípade JPEG alebo PNG sa aplikuje funkcia na úpravu úrovní. Pri FITS a XISF
sa dáta skonvertujú bez zmeny úrovní.</p>
<h3>Zobrazenie</h3>
<p>Menu <i>Zobrazenie</i> ovplyvňuje veľkosť a škálu zobrazovaných obrázkov.
<i>Priblížiť/Oddialiť</i> zväčší a zmenší obrázok. Na toto tiež slúži aj kolečko myši. <i>Najlepšia veľkosť</i> zobrazí
obrázok tak aby bol zobrazený na celú plochu. <i>100%</i> zobrazí obrázok v pomere 1:1. <i>Celá obrazovka</i> zobrazí
hlavné okno na celú obrazovku. <i>Náhľady</i> zobrazí malé náhľady pre všetky obrázky z aktuálneho adresára.
</p>
<h3>Panel úrovní</h3>
<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"></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>
<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.
Falošné farby zobraí čiernobiele obrázky vo farebnej škále.
Prevoď CFA na farbu prevedie demozaikovanie čiernobieleho obrázku na farebný.
Posledné tlačidlo zapína a vypína nastavovanie optimálnych hodnôt úrovní pre každý obrázok zvlášť.</p>
<h3>Označovanie obrázkov</h3>
<p>Obrázky je možné si označiť cez menu <i>Výber</i>. Zoznam takto označených obrázkov sa dá zobraziť cez
<i>Výber->Ukázať označené</i>. V tomto dialógu sa dá tiež tento zoznam upraviť. Okrem toho sa pri označených
obrázkoch zobrazuje znak * v záhlaví hlavného okna. Takto označené obrázky je potom možné skopírovať alebo
presunúť do vybraného adresára pomocou <i>Súbor->Skopírovať/Presunúť označené súbory</i>. Po skopírovaní alebo
presunutú sa zoznam označených obrázkov vymaže. Program si tento zoznam pamätá aj po svojom ukončení.</p>
<p>Ďalší spôsob ako označiť obrázky je cez databázu FITS/XISF kde je možné vybrať jednotlivé riadky. Potom stačí
vybrať označit alebo odznačiť v kontextovom menu. Označené súbory budú zobrazené tučným textom. Tretí spôsob na označenie
obrázkov je možné cez náhľady. Držaním <i>Shift</i> a následne kliknutím ľavým tlačítkom myši sa daný obrázok označí.
Pre odznačenie je treba držať <i>Ctrl</i></p>
<h3>Databáza FITS/XISF súborov</h3>
<p>Program vie prehľadať adresár a indexovať meta údaje z FIST a XISF obrázkov do internej databázy v ktorej sa dá
následne vyhľadávať. Najprv je treba indexovať adresár s FIST/XISF obrázkami <i>Súbor->Indexovať adresár</i>.
Vybraný adresár je prehľadaný a meta údaje z FIST a XISF obrázkov sú uložené do databázy. Na kontrolu a obnovu datábazy
je možné spustiť re-indexáciu <i>Súbor->Reindex</i>. Toto obnoví zmenené údaje a odstráni záznamy o už neexistujúcich súboroch.
Pre indexovanie nových súborov je treba znova pustiť indexáciu.</p>
<p>Pre zobrazenie databázy je treba zobraziť jej panel cez <i>Dokovacie panely->Databáza FITS/XISF súborov</i>. Databáza je zobrazená vo forme tabuľky
kde sú jednotlivé stĺpcoch zobrazené vybrané vlastnosti. V spodnej časti panelu je tlačidlo ktoré zobrazí dialóg na výber zobrazovaných
sĺpcov. Nasledujú tri výberové a textové polia. Tieto slúžia na vyhľadávanie v databáze. Výberové pole určuje stĺpec v ktorom sa
má vyhľadávať a do textového poľa sa zadáva hodnota na vyhľadanie.
<p>Zastupné znaky:
<ul>
<li><b>%</b> (percent) je zastupný znak reprezentujúci žiadný alebo hocikoľko znakov.</li>
<li><b>_</b> (underscore) je zastupný znak nahrádzajúci presne jeden znak.</i>
<li>Bez zástupných znako sa hľadá presná zhoda.</li>
</ul>
</p>
<br><img src=":/about/filter.png"><br>
V nasledovnom príklade sa vyhľadajú súbory ktoré majú v mene súboru "Bias", OBJECT je M_42 a DATE začína reťazcom 2022. Znak % sa berie ako
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>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>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.
Do zoznamu môžete pridať adresáre alebo jednotlivé súbory. Adresáre sú rekurzívne prehľadané na všetky súbory. Zoznam súborov je potom predaný do skriptu ako pole nazvané <b>files</b>.
V skripte potom cez toto pole iteruje nasledovne.
<pre>for(file in files)
{
if(file.suffix() == "fits")
{
core.log(file.fileName());
file.convert(file.relativeFilePath(), "XISF");
}
}
</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>
<li><b>log(message)</b> vypíše message do okna záznamu.</li>
<li><b>mark(file)</b> označí súbor rovnako ako cez GUI. Parameter je objekt typu <i>File</i>.</li>
<li><b>unmark(file)</b> odznačí súbor rovnako ako cez GUI. Parameter je objekt typu <i>File</i>.</li>
<li><b>isMarked(file)</b> overenie či je súbor označený. Parameter je objekt typu <i>File</i>.</li>
<li><b>setMaxThread(maxthread)</b> nastavý maximálny počet paralelných vlákien pri vykonávaní asynchrónnych úloh.</li>
<li><b>sync()</b> počká kým sa dokončia všetky asynchrónne úlohy.</li>
<li><b>getString(label = "", text = "")</b> ukáže dialóg pre získanie textovej hodnoty od používateľa. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Obydva parametre sú voliteľné takže aj volanie <i>getString()</i> je valdiné.
Metoda vracia textový retazec alebo Undefined ak bolo stlačené tlačidlo zrušiť.</li>
<li><b>getInt(label = "", value = 0)</b> ukáže diálog pre získanie celočíselnej hodnoty. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Obydva parametre sú voliteľné. Vracia zadané číslo alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
<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>
V poli <b>files</b> sú inštancie objektu typu <b>File</b> ktorý ma nasledovné metódy.
<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>
<li><b>absolutePath()</b> returns an absolute path without the file name</li>
<li><b>relativeFilePath()</b> return relative path including file name relative to directory that was in list of directories to be scanned. For example you add C:/images as input directory. In this directory there
is file <i>C:/images/lights/red/M42_001.fits</i> then this method will return <i>lights/red/M42_001.fits</i></li>
<li><b>relativePath()</b> return same path as previous method just without file name. <i>lights/red</i></li>
<li><b>baseName()</b> return file name up to the first dot. For example for <i>some.file.name.fits</i> it will return <i>some</i></li>
<li><b>completeBaseName()</b> return file name up to the last dot. For example for <i>some.file.name.fits</i> it will return <i>some.file.name</i></li>
<li><b>suffix()</b> return string after last dot in file name. For example <i>fits</i></li>
<li><b>size()</b> return size of file in bytes.</li>
<li><b>fitsKeywords()</b> return array of strings with every keyword that is in header. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
<li><b>fitsValue(key)</b> return value for keyword. In case that there is multiple occurrences it return last one.</li>
<li><b>fitsValues(key)</b> return array of values for keyword.</li>
<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
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", "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.
It return new instance of <i>File</i> that point to converted file.
<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.
<pre>let compression = {"compressionType": "zstd+sh"};
let convertedFiles = [];
for(file of files)
{
if(file.suffix() == "fits")
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
}
core.sync(); // ensure that files exist
for(file of convertedFiles)// now we can iterate over the files
{
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
}</pre></li>
<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.
<pre>let modify = new FITSRecordModify();
modify.updateKeyword("OBJECT", "M42");
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
modify.removeKeyword("OBJECT");
// doesn't matter that it is specified as last. This will first remove
// existing OBJECT record and then add again OBJECT=M42
for(file in files)
{
file.modifyFITSRecords(modify);
}</pre>
<ul>
<li><b>new FITSRecordModify()</b> create new instance of object.</li>
<li><b>removeKeyword(key);</b> specify removing of record with <i>key</i> as keyword.</li>
<li><b>updateKeyword(key, value, comment = "")</b> specify updating existing keyword with value and comment. Comment is optional parameter. If record with keyword doesn't exist then it will add new one.
Unless you want to have multiple records with same keyword (for example HISTORY) always use this method and not addKeyword.</li>
<li><b>addKeyword(key, value, comment = "")</b> specify adding new keyword</li>
</ul>
<p><small>PS: Kanji v ikone programu znamená "astronomia" v Japončine</small></p>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+20
View File
@@ -0,0 +1,20 @@
<table><tr>
<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>
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>
the Free Software Foundation, either version 3 of the License, or<br>
(at your option) any later version.<br><br>
This program is distributed in the hope that it will be useful,<br>
but WITHOUT ANY WARRANTY; without even the implied warranty of<br>
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the<br>
GNU General Public License for more details.<br><br>
You should have received a copy of the GNU General Public License<br>
along with this program. If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
</td>
</tr></table>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

-207
View File
@@ -1,207 +0,0 @@
#include "database.h"
#include <QStandardPaths>
#include <QDir>
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
#include "loadrunable.h"
Database::Database(QObject *parent) : QObject(parent)
{
}
bool Database::init()
{
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(path);
m_database = QSqlDatabase::addDatabase("QSQLITE");
if(!dir.mkpath("."))
return false;
if(m_database.isValid())
{
m_database.setDatabaseName(dir.absoluteFilePath("database.db"));
if(m_database.open())
{
m_database.exec("PRAGMA foreign_keys = ON");
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)");
m_database.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)");
QSqlError error = m_database.lastError();
if(error.type() == QSqlError::NoError)
{
m_markQuery = QSqlQuery(m_database);
m_markQuery.prepare("INSERT INTO files (file) VALUES (?)");
m_unmarkQuery = QSqlQuery(m_database);
m_unmarkQuery.prepare("DELETE FROM files WHERE file = (?)");
m_isMarkedQuery = QSqlQuery(m_database);
m_isMarkedQuery.prepare("SELECT * FROM files WHERE file = (:name)");
m_insertFile = QSqlQuery(m_database);
m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
m_insertFitsHeader = QSqlQuery(m_database);
m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
m_checkFile = QSqlQuery(m_database);
m_checkFile.prepare("SELECT id,mtime FROM fits_files WHERE file=?");
m_headerKeywords = QSqlQuery(m_database);
m_headerKeywords.prepare("SELECT DISTINCT key FROM fits_headers ORDER BY key");
m_deleteFile = QSqlQuery(m_database);
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
return true;
}
qDebug() << error.text();
}
}
return false;
}
bool Database::mark(const QString &filename)
{
m_markQuery.bindValue(0, filename);
m_markQuery.exec();
return checkError();
}
bool Database::unmark(const QString &filename)
{
m_unmarkQuery.bindValue(0, filename);
m_unmarkQuery.exec();
return checkError();
}
bool Database::isMarked(const QString &filename)
{
m_isMarkedQuery.bindValue(":name", filename);
m_isMarkedQuery.exec();
checkError();
return m_isMarkedQuery.next();
}
QStringList Database::getMarkedFiles()
{
QSqlQuery markedFiles("SELECT * from files", m_database);
QStringList files;
while(markedFiles.next())
{
files << markedFiles.value("file").toString();
}
qDebug() << files.size();
return files;
}
bool Database::checkError()
{
QSqlError error = m_database.lastError();
if(error.type() == QSqlError::NoError)
return true;
else
{
qDebug() << error.text();
return false;
}
}
static QStringList nameFilters = {"*.fit", "*.fits"};
static int countFiles(const QDir &dir, int count = 0)
{
count += dir.entryList(nameFilters, QDir::Files).size();
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for(const QString &d : dirs)
count += countFiles(dir.filePath(d));
return count;
}
void Database::indexDir(const QDir &dir, QProgressDialog *progress)
{
m_progress = 0;
int count = countFiles(dir);
progress->setMaximum(count);
m_database.transaction();
if(indexDir2(dir, progress))
m_database.commit();
else
m_database.rollback();
}
QStringList Database::getFitsKeywords()
{
m_headerKeywords.exec();
QStringList keywords;
while(m_headerKeywords.next())
{
keywords << m_headerKeywords.value(0).toString();
}
return keywords;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
{
QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for(const QString &d : dirs)
{
if(!indexDir2(dir.filePath(d), progress))
return false;
}
ImageInfoData info;
for(const QFileInfo &file : files)
{
progress->setValue(m_progress++);
if(progress->wasCanceled())return false;
QString filePath = file.absoluteFilePath();
QString mtime = file.fileTime(QFileDevice::FileModificationTime).toString(Qt::ISODate);
m_checkFile.bindValue(0, filePath);
m_checkFile.exec();
if(m_checkFile.next())
{
if(m_checkFile.value(1).toString() == file.fileTime(QFileDevice::FileModificationTime).toString(Qt::ISODate))
continue;
else
{
m_deleteFile.bindValue(0, m_checkFile.value(0).toLongLong());
m_deleteFile.exec();
}
}
readFITSHeader(filePath, info);
m_insertFile.bindValue(0, filePath);
m_insertFile.bindValue(1, mtime);
if(!m_insertFile.exec())
{
qDebug() << m_insertFile.lastError();
return false;
}
qlonglong last_id = m_insertFile.lastInsertId().toLongLong();
QVariantList file_id, keys, values, comments;
for(auto &record : info.fitsHeader)
{
file_id << last_id;
keys << QString(record.key);
values << record.value.toString();
comments << QString(record.comment);
}
m_insertFitsHeader.bindValue(0, file_id);
m_insertFitsHeader.bindValue(1, keys);
m_insertFitsHeader.bindValue(2, values);
m_insertFitsHeader.bindValue(3, comments);
if(!m_insertFitsHeader.execBatch())
{
qDebug() << m_insertFitsHeader.lastError();
return false;
}
qDebug() << filePath << last_id;
info.fitsHeader.clear();
}
return true;
}
-180
View File
@@ -1,180 +0,0 @@
#include "databaseview.h"
#include <QVBoxLayout>
#include <QPushButton>
#include <QSettings>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QSqlError>
#include <QDebug>
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
SelectColumnsDialog::SelectColumnsDialog(QWidget *parent) : QDialog(parent)
{
m_listWidget = new QListWidget(this);
m_listWidget->setSelectionMode(QAbstractItemView::MultiSelection);
QVBoxLayout *layout = new QVBoxLayout(this);
setLayout(layout);
layout->addWidget(m_listWidget);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttonBox);
setWindowTitle(tr("Select columns"));
}
void SelectColumnsDialog::setColumns(QStringList columns)
{
QSettings settings;
m_listWidget->addItems(columns);
QStringList selected = settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList();
for(auto &sel : selected)
{
int i = columns.indexOf(sel);
if(i>=0)
m_listWidget->item(i)->setSelected(true);
}
}
QStringList SelectColumnsDialog::selectedColumns()
{
QStringList ret;
for(auto &sel : m_listWidget->selectedItems())
ret.append(sel->text());
return ret;
}
FITSFileModel::FITSFileModel(QObject *parent) : QSqlQueryModel(parent)
{
}
void FITSFileModel::sort(int column, Qt::SortOrder order)
{
if(column < 0)
m_sort.clear();
else if(column <= m_columns.size())
{
if(column == 0)m_sort = " ORDER BY file ";
else m_sort = QString(" ORDER BY \"%1\" ").arg(m_columns.at(column-1));
m_sort += order == Qt::AscendingOrder ? "ASC" : "DESC";
prepareQuery();
}
}
void FITSFileModel::setColumns(const QStringList &columns)
{
m_columns = columns;
prepareQuery();
}
void FITSFileModel::setFilter(const QString &key, const QString &value)
{
if(value.isEmpty())
{
m_having.clear();
}
else
{
if(!m_columns.contains(key))m_columns.append(key);
m_having = QString(" HAVING \"%1\" LIKE \"%2\"").arg(key).arg(value);
}
prepareQuery();
}
void FITSFileModel::prepareQuery()
{
QString sql = "SELECT file,";
for(auto &column : m_columns)
{
sql += QString("GROUP_CONCAT(CASE WHEN key=\"%1\" THEN value END) AS \"%1\",").arg(column);
}
sql.chop(1);
sql += " FROM fits_files LEFT JOIN fits_headers ON fits_files.id=id_file GROUP BY fits_files.id" + m_having + m_sort;
setQuery(sql);
qDebug() << sql;
if(lastError().type() != QSqlError::NoError)
qDebug() << lastError();
}
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
,m_database(database)
{
QVBoxLayout *layout = new QVBoxLayout(this);
setLayout(layout);
m_tableView = new QTableView(this);
m_tableView->verticalHeader()->setDefaultSectionSize(1);
m_tableView->setSortingEnabled(true);
m_tableView->horizontalHeader()->setSortIndicatorShown(true);
layout->addWidget(m_tableView);
connect(m_tableView, &QTableView::activated, this, &DataBaseView::itemActivated);
m_model = new FITSFileModel(this);
QSettings settings;
m_tableView->setModel(m_model);
m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
m_tableView->horizontalHeader()->restoreState(settings.value("databaseview/header").toByteArray());
QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout);
QPushButton *selectColumnsButton = new QPushButton(tr("Select columns"), this);
hlayout->addWidget(selectColumnsButton);
connect(selectColumnsButton, &QPushButton::pressed, this, &DataBaseView::selectColumns);
m_filterKeyword = new QComboBox(this);
m_filterKeyword->addItem("file");
m_filterKeyword->addItems(m_database->getFitsKeywords());
m_search = new QLineEdit(this);
m_search->setPlaceholderText(tr("Text to search, you can % as wildcard"));
connect(m_search, &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
connect(filterButton, SIGNAL(pressed()), this, SLOT(applyFilter()));
hlayout->addWidget(m_filterKeyword);
hlayout->addWidget(m_search);
hlayout->addWidget(filterButton);
}
DataBaseView::~DataBaseView()
{
QSettings settings;
settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
}
void DataBaseView::selectColumns()
{
SelectColumnsDialog dialog;
QStringList columns = m_database->getFitsKeywords();
dialog.setColumns(columns);
if(dialog.exec() == QDialog::Accepted)
{
QSettings settings;
QStringList columns = dialog.selectedColumns();
settings.setValue("databaseview/selectedColumns", columns);
m_model->setColumns(columns);
}
}
void DataBaseView::loadDatabase()
{
QSettings settings;
m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
}
void DataBaseView::itemActivated(const QModelIndex &index)
{
emit loadFile(m_model->data(index.siblingAtColumn(0)).toString());
}
void DataBaseView::applyFilter()
{
m_model->setFilter(m_filterKeyword->currentText(), m_search->text());
}
-58
View File
@@ -1,58 +0,0 @@
#ifndef DATABASEVIEW_H
#define DATABASEVIEW_H
#include <QDialog>
#include <QWidget>
#include <QSqlQueryModel>
#include <QTableView>
#include <QListWidget>
#include <QComboBox>
#include <QLineEdit>
#include "database.h"
class SelectColumnsDialog : public QDialog
{
Q_OBJECT
QListWidget *m_listWidget;
public:
SelectColumnsDialog(QWidget *parent = nullptr);
void setColumns(QStringList columns);
QStringList selectedColumns();
};
class FITSFileModel : public QSqlQueryModel
{
Q_OBJECT
QStringList m_columns;
QString m_sort;
QString m_having;
public:
FITSFileModel(QObject *parent = nullptr);
void sort(int column, Qt::SortOrder order);
void setColumns(const QStringList &columns);
void setFilter(const QString &key, const QString &value);
protected:
void prepareQuery();
};
class DataBaseView : public QWidget
{
Q_OBJECT
Database *m_database;
QTableView *m_tableView;
FITSFileModel *m_model;
QComboBox *m_filterKeyword;
QLineEdit *m_search;
public:
explicit DataBaseView(Database *database, QWidget *parent = nullptr);
~DataBaseView();
public slots:
void selectColumns();
void loadDatabase();
void itemActivated(const QModelIndex &index);
void applyFilter();
signals:
void loadFile(QString file);
};
#endif // DATABASEVIEW_H
-36
View File
@@ -1,36 +0,0 @@
#include "filesystemwidget.h"
#include <QSettings>
#include <QVBoxLayout>
FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) : QWidget(parent)
, m_model(model)
{
m_listView = new QListView(this);
m_listView->setModel(model);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_listView);
setLayout(layout);
connect(m_listView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(fileClicked(QModelIndex)));
}
void FilesystemWidget::setDir(const QString &dir)
{
//m_model->setRootPath(dir);
//m_treeView->setRootIndex(m_model->index(m_model->rootPath()));
}
void FilesystemWidget::selectFile(int row)
{
QModelIndex index = m_model->index(row, 0);
m_listView->selectionModel()->clearSelection();
m_listView->selectionModel()->select(index, QItemSelectionModel::SelectCurrent);
m_listView->scrollTo(index);
}
void FilesystemWidget::fileClicked(const QModelIndex &index)
{
emit fileSelected(index.row());
}
-23
View File
@@ -1,23 +0,0 @@
#ifndef FILESYSTEMWIDGET_H
#define FILESYSTEMWIDGET_H
#include <QWidget>
#include <QFileSystemModel>
#include <QListView>
class FilesystemWidget : public QWidget
{
Q_OBJECT
QListView *m_listView;
QAbstractItemModel *m_model;
public:
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
void setDir(const QString &dir);
private slots:
void selectFile(int row);
void fileClicked(const QModelIndex &index);
signals:
void fileSelected(int row);
};
#endif // FILESYSTEMWIDGET_H
+19
View File
@@ -0,0 +1,19 @@
set(_build_version "unknown")
find_package(Git)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags HEAD
WORKING_DIRECTORY "${local_dir}"
OUTPUT_VARIABLE _build_version
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "GIT hash: ${_build_version}")
else()
message(STATUS "GIT not found")
endif()
message(STATUS "local:${local_dir} output:${output_dir}")
configure_file("${local_dir}/gitversion.h.in" "${output_dir}/gitversion.h" @ONLY)
+6
View File
@@ -0,0 +1,6 @@
#ifndef GITVERSION_H
#define GITVERSION_H
#define GITVERSION "@_build_version@"
#endif
-27
View File
@@ -1,27 +0,0 @@
#version 130
uniform sampler2D qt_Texture0;
in vec2 qt_TexCoord0;
uniform vec3 mtf_param;
uniform bool bw;
uniform bool invert;
out vec4 color;
vec4 MTF(vec4 x, vec3 m)
{
x = (x - m.x) / (m.z - m.x);
x = clamp(x, vec4(0.0), vec4(1.0));
return ((m.y - 1) * x) / ((2 * m.y - 1) * x - m.y);
}
void main(void)
{
color = texture2D(qt_Texture0, qt_TexCoord0);
if(bw)color = color.rrra;
color = MTF(color, mtf_param);
if(invert)color = vec4(1.0) - color;
if(any(lessThan(qt_TexCoord0, vec2(0.0))) || any(greaterThan(qt_TexCoord0, vec2(1.0))))
color = vec4(0.0);
}
-40
View File
@@ -1,40 +0,0 @@
#ifndef IMAGEINFO_H
#define IMAGEINFO_H
#include <QTreeWidget>
struct FITSRecord
{
QByteArray key;
QVariant value;
QByteArray comment;
bool editable() const;
};
struct ImageInfoData
{
QVector<FITSRecord> fitsHeader;
QVector<QPair<QString, QString>> info;
};
Q_DECLARE_METATYPE(ImageInfoData);
typedef enum
{
None,
Statistics,
Peaks,
Stars,
}AnalyzeLevel;
class ImageInfo : public QTreeWidget
{
Q_OBJECT
public:
explicit ImageInfo(QWidget *parent);
~ImageInfo();
public slots:
void setInfo(const ImageInfoData &info);
};
#endif // IMAGEINFO_H
-314
View File
@@ -1,314 +0,0 @@
#include "imageringlist.h"
#include <QThreadPool>
#include <QDir>
#include "loadrunable.h"
#include "rawimage.h"
using namespace std;
const int DEFAULT_WIDTH = 2;
Image::Image(const QString name, ImageRingList *ringList) :
m_loading(false),
m_released(true),
m_current(false),
m_name(name),
m_ringList(ringList)
{
}
void Image::load()
{
if(!m_rawImage && !m_loading)
{
m_loading = true;
m_released = false;
QThreadPool::globalInstance()->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel()));
}
if(!m_loading && m_rawImage)
emit pixmapLoaded(this);
}
void Image::release()
{
m_rawImage.reset();
m_released = true;
m_loading = false;
}
QString Image::name() const
{
return m_name;
}
RawImage *Image::rawImage()
{
return m_rawImage.get();
}
ImageInfoData Image::info() const
{
return m_info;
}
bool Image::isCurrent() const
{
return !m_released;
}
void Image::imageLoaded(void *rawImage, ImageInfoData info)
{
m_loading = false;
if(!m_released)
{
m_rawImage.reset(static_cast<RawImage*>(rawImage));
m_info = info;
emit pixmapLoaded(this);
}
else
{
delete static_cast<RawImage*>(rawImage);
}
}
ImageRingList::ImageRingList(QObject *parent) : QAbstractItemModel(parent)
, m_liveMode(false)
, m_analyzeLevel(None)
{
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
}
ImageRingList::~ImageRingList()
{
QThreadPool::globalInstance()->clear();
QThreadPool::globalInstance()->waitForDone();
}
bool ImageRingList::setDir(const QString path, const QString &currentFile)
{
QDir dir(path);
if(dir.exists())
{
QStringList nameFilter;
nameFilter << "*.jpg" << "*.jpeg" << "*.png" << "*.cr2" << "*.fit" << "*.fits";
QStringList list = dir.entryList(nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name);
QStringList absolutePaths;
foreach(const QString &file, list)
{
absolutePaths.append(dir.absoluteFilePath(file));
}
setFiles(absolutePaths, m_liveMode ? list.first() : currentFile);
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
m_fileSystemWatcher.addPath(path);
return true;
}
return false;
}
void ImageRingList::setFile(const QString &file)
{
QFileInfo info(file);
if(info.isDir())
setDir(file);
else
setDir(info.absolutePath(), file);
}
ImagePtr ImageRingList::currentImage()
{
if(m_images.size())
return *m_currImage;
else
return 0;
}
void ImageRingList::increment()
{
if(m_images.size())
{
(*m_firstImage)->release();
m_firstImage = increment(m_firstImage);
m_currImage = increment(m_currImage);
(*m_currImage)->load();
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load();
}
}
void ImageRingList::decrement()
{
if(m_images.size())
{
(*m_lastImage)->release();
m_firstImage = decrement(m_firstImage);
m_currImage = decrement(m_currImage);
(*m_currImage)->load();
m_lastImage = decrement(m_lastImage);
(*m_firstImage)->load();
}
}
void ImageRingList::setLiveMode(bool live)
{
m_liveMode = live;
}
void ImageRingList::setCalculateStats(bool stats)
{
m_analyzeLevel = stats ? Statistics : None;
}
void ImageRingList::setFindPeaks(bool findPeaks)
{
m_analyzeLevel = findPeaks ? Peaks : None;
}
void ImageRingList::setFindStars(bool findStars)
{
m_analyzeLevel = findStars ? Stars : None;
}
AnalyzeLevel ImageRingList::analyzeLevel() const
{
return m_analyzeLevel;
}
void ImageRingList::loadFile(int row)
{
if(row < m_images.size())
{
m_firstImage = m_currImage = m_lastImage = m_images.begin()+row;
if(m_images.empty())
return;
(*m_currImage)->load();
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
if(m_liveMode)
m_width = 0;
for(int i=0; i<m_width; i++)
{
m_firstImage = decrement(m_firstImage);
(*m_firstImage)->load();
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load();
}
if(m_lastImage != m_firstImage)
{
QList<ImagePtr>::iterator iter = increment(m_lastImage);
while(m_firstImage != iter)
{
(*iter)->release();
iter = increment(iter);
}
}
}
}
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
{
return createIndex(row, column, m_images.at(row).get());
}
QModelIndex ImageRingList::parent(const QModelIndex &child) const
{
return QModelIndex();
}
int ImageRingList::rowCount(const QModelIndex &parent) const
{
if(parent == QModelIndex())
return m_images.size();
else
return 0;
}
int ImageRingList::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant ImageRingList::data(const QModelIndex &index, int role) const
{
switch(role)
{
case Qt::DisplayRole:
{
QFileInfo info(m_images.at(index.row())->name());
return info.fileName();
}
default:
return QVariant();
}
}
QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int role) const
{
if(section==0 && orientation==Qt::Horizontal && role==Qt::DisplayRole)
{
return tr("Name");
}
return QVariant();
}
void ImageRingList::setFiles(const QStringList files, const QString &currentFile)
{
QThreadPool::globalInstance()->clear();
QThreadPool::globalInstance()->waitForDone();
beginResetModel();
m_images.clear();
foreach(const QString &file, files)
{
ImagePtr ptr = make_shared<Image>(file, this);
connect(ptr.get(), SIGNAL(pixmapLoaded(Image*)), this, SLOT(imageLoaded(Image*)));
m_images.append(ptr);
}
int index = files.indexOf(currentFile);
if(index < 0)
index = 0;
endResetModel();
loadFile(index);
}
QList<ImagePtr>::iterator ImageRingList::increment(QList<ImagePtr>::iterator iter)
{
iter++;
if(iter == m_images.end())
iter = m_images.begin();
return iter;
}
QList<ImagePtr>::iterator ImageRingList::decrement(QList<ImagePtr>::iterator iter)
{
if(iter == m_images.begin())
iter = m_images.end();
iter--;
return iter;
}
void ImageRingList::imageLoaded(Image *image)
{
if(image->name() == (*m_currImage)->name())
{
emit pixmapLoaded(image);
emit infoLoaded(image->info());
emit currentImageChanged(m_currImage-m_images.begin());
}
}
void ImageRingList::dirChanged(QString dir)
{
QString currentFile;
if(m_images.size())
currentFile = (*m_currImage)->name();
setDir(dir, currentFile);
emit currentImageChanged(m_currImage-m_images.begin());
}
-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->posF()) / 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->posF();
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);
void keyReleaseEvent(QKeyEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void resizeEvent(QResizeEvent *event);
void wheelEvent(QWheelEvent *event);
};
#endif // IMAGESCROLLAREA_H
-390
View File
@@ -1,390 +0,0 @@
#include "imagescrollareagl.h"
#include <QOpenGLFunctions>
#include <QDebug>
#include <QKeyEvent>
#include <QOpenGLDebugLogger>
#include <QOpenGLPixelTransferOptions>
#include <QOpenGLFramebufferObject>
#include <QGridLayout>
struct RawImageType
{
QOpenGLTexture::PixelFormat pixelFormat;
QOpenGLTexture::TextureFormat textureFormat;
QOpenGLTexture::PixelType dataType;
bool bw;
};
const RawImageType rawImageTypes[] = {
{QOpenGLTexture::Red, QOpenGLTexture::R8_UNorm, QOpenGLTexture::UInt8, true},
{QOpenGLTexture::Red, QOpenGLTexture::R16_UNorm, QOpenGLTexture::UInt16, true},
{QOpenGLTexture::Red, QOpenGLTexture::R32F, QOpenGLTexture::Float32, true},
{QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
{QOpenGLTexture::BGRA,QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
{QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
{QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
};
void setScrollRange(QScrollBar *scrollBar, int newRange)
{
int page = scrollBar->pageStep();
int pos = scrollBar->value() + page/2;
int range = scrollBar->maximum() + page;
float relPos = (float)pos/(float)range;
if(page >= newRange)
scrollBar->hide();
else
scrollBar->show();
scrollBar->setRange(0, newRange - page);
scrollBar->setValue(relPos*newRange - page/2);
}
ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
{
setFocusPolicy(Qt::ClickFocus);
m_range = UINT16_MAX;
m_low = 0;
m_mid = 0.5;
m_high = 1;
m_dx = m_dy = 0;
m_scale = 1.0f;
m_blockRepaint = false;
m_range = UINT16_MAX;
m_imgWidth = m_imgHeight = -1;
m_superpixel = m_invert = false;
}
ImageWidget::~ImageWidget()
{
makeCurrent();
}
void ImageWidget::setImage(const RawImage *image)
{
if(image == nullptr)return;
m_imgWidth = image->width();
m_imgHeight = image->height();
const RawImageType &rawImageType = rawImageTypes[image->type()];
m_image->destroy();
m_image->setFormat(rawImageType.textureFormat);
m_image->setSize(image->width(), image->height());
m_image->setMipLevels([&](){ int c = 0; int s = std::min(m_imgWidth, m_imgHeight); while(s>>=1)c++; return c; }());
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, image->data(), m_transferOptions.get());
m_image->setLevelOfDetailRange(m_superpixel ? 1 : 0, m_image->mipMaxLevel());
m_image->generateMipMaps();
m_bwImg = rawImageType.bw;
update();
}
void ImageWidget::setImage(const QPixmap &pixmap)
{
QImage img = pixmap.toImage();
m_imgWidth = pixmap.width();
m_imgHeight = pixmap.height();
m_image->destroy();
m_image->setData(img);
m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
m_image->setWrapMode(QOpenGLTexture::ClampToBorder);
m_image->setBorderColor(0, 0, 0, 0);
m_bwImg = false;
update();
}
void ImageWidget::setScale(float scale)
{
m_scale = scale;
update();
}
void ImageWidget::blockRepaint(bool block)
{
m_blockRepaint = block;
if(!block)update();
}
void ImageWidget::setMTFParams(float low, float mid, float high)
{
m_low = low;
m_mid = mid;
m_high = high;
update();
}
void ImageWidget::setOffset(int dx, int dy)
{
m_dx = dx;
m_dy = dy;
update();
}
void ImageWidget::superPixel(bool enable)
{
m_superpixel = enable;
m_image->setLevelOfDetailRange(enable ? 1 : 0, m_image->mipMaxLevel());
update();
}
void ImageWidget::invert(bool enable)
{
m_invert = enable;
update();
}
QImage ImageWidget::renderToImage()
{
if(m_imgWidth < 0)return QImage();
makeCurrent();
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight);
fbo.bind();
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
m_program->bind();
m_program->setUniformValue("viewport", (float)m_imgWidth, (float)m_imgHeight);
m_program->setUniformValue("offset", 0.0f, 0.0f);
m_program->setUniformValue("zoom", 1.0f);
m_image->bind(0);
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
fbo.bindDefault();
return fbo.toImage(true);
}
void ImageWidget::paintGL()
{
if(m_blockRepaint)return;
float dx = m_dx;
float dy = m_dy;
if(width() > m_image->width()*m_scale)
dx = -width()*0.5f + m_image->width()*m_scale*0.5f;
if(height() > m_image->height()*m_scale)
dy = -height()*0.5f + m_image->height()*m_scale*0.5f;
m_program->bind();
m_program->setUniformValue("viewport", (float)width(), (float)height());
m_program->setUniformValue("offset", dx, dy);
m_program->setUniformValue("mtf_param", m_low, m_mid, m_high);
m_program->setUniformValue("zoom", 1.0f/m_scale);
m_program->setUniformValue("bw", m_bwImg);
m_program->setUniformValue("invert", m_invert);
m_image->bind(0);
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
void ImageWidget::resizeGL(int w, int h)
{
m_width = w;
m_height = h;
f->glViewport(0, 0, w, h);
}
void ImageWidget::initializeGL()
{
f = context()->functions();
f->glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
QOpenGLFunctions_3_2_Core *f3 = context()->versionFunctions<QOpenGLFunctions_3_2_Core>();
m_vao = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
m_vao->create();
m_vao->bind();
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
logger->initialize();
logger->startLogging();
connect(logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage &message)
{
qDebug() << message;
});
qDebug() << (char*)f->glGetString(GL_VENDOR);
qDebug() << (char*)f->glGetString(GL_RENDERER);
qDebug() << (char*)f->glGetString(GL_VERSION);
qDebug() << context()->format();
// each vertex is x,y 2D position and s,t texture coordinates
float vertexs[] = {-1.0f, -1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 0.0f,};
m_buffer = std::unique_ptr<QOpenGLBuffer>(new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer));
m_buffer->setUsagePattern(QOpenGLBuffer::StaticDraw);
m_buffer->create();
m_buffer->bind();
m_buffer->allocate(vertexs, sizeof(vertexs));
f->glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(float)*4, 0);
m_program = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/image.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/image.frag");
if(f3)f3->glBindFragDataLocation(m_program->programId(), 0, "color");
if(!m_program->link())
{
qDebug() << "Link failed" << m_program->log();
}
m_program->bind();
m_program->enableAttributeArray("qt_Vertex");
m_program->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
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("scale", 1.0f, 0.0f);
m_image = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2D));
m_image->setFormat(QOpenGLTexture::RGB8U);
m_image->allocateStorage();
m_image->bind(0);
m_image->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_image->setMagnificationFilter(QOpenGLTexture::Linear);
m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
m_transferOptions->setAlignment(1);
}
ImageScrollAreaGL::ImageScrollAreaGL(QWidget *parent) : QWidget(parent)
{
QGridLayout *layout = new QGridLayout(this);
setLayout(layout);
m_imageWidget = new ImageWidget(this);
m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
m_scale = 1.0f;
m_bestFit = false;
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()));
}
ImageScrollAreaGL::~ImageScrollAreaGL()
{
}
void ImageScrollAreaGL::setImage(RawImage *image)
{
m_imageWidget->setImage(image);
m_imgWidth = image->width();
m_imgHeight = image->height();
if(m_bestFit)bestFit();
updateScrollbars();
}
void ImageScrollAreaGL::setImage(const QPixmap &pixmap)
{
m_imageWidget->setImage(pixmap);
m_imgWidth = pixmap.width();
m_imgHeight = pixmap.height();
if(m_bestFit)bestFit();
updateScrollbars();
}
ImageWidget *ImageScrollAreaGL::imageWidget()
{
return m_imageWidget;
}
void ImageScrollAreaGL::updateScrollbars(bool zoom)
{
if(zoom)
{
setScrollRange(m_verticalScrollBar, m_imgHeight*m_scale);
setScrollRange(m_horizontalScrollBar, m_imgWidth*m_scale);
}
else
{
m_verticalScrollBar->setRange(0, m_imgHeight*m_scale - m_verticalScrollBar->pageStep());
m_horizontalScrollBar->setRange(0, m_imgWidth*m_scale - m_horizontalScrollBar->pageStep());
}
}
void ImageScrollAreaGL::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
m_verticalScrollBar->setPageStep(m_imageWidget->height());
m_horizontalScrollBar->setPageStep(m_imageWidget->width());
updateScrollbars();
}
void ImageScrollAreaGL::mouseMoveEvent(QMouseEvent *event)
{
QPoint delta = m_lastPos - event->pos();
m_horizontalScrollBar->setValue(m_horizontalScrollBar->value() + delta.x());
m_verticalScrollBar->setValue(m_verticalScrollBar->value() + delta.y());
m_lastPos = event->pos();
}
void ImageScrollAreaGL::mousePressEvent(QMouseEvent *event)
{
m_lastPos = event->pos();
}
void ImageScrollAreaGL::wheelEvent(QWheelEvent *event)
{
m_bestFit = false;
if(event->angleDelta().y() != 0)
zoom(event->angleDelta().y() / 1200.0f);
}
void ImageScrollAreaGL::zoom(float delta)
{
if((m_scale >= 8.0f && delta > 0) || (m_scale <= 0.1f && delta < 0))return;
m_scale += delta;
m_imageWidget->blockRepaint(true);
m_imageWidget->setScale(m_scale);
updateScrollbars(true);
m_imageWidget->blockRepaint(false);
}
void ImageScrollAreaGL::zoomIn()
{
zoom(0.1f);
m_bestFit = false;
}
void ImageScrollAreaGL::zoomOut()
{
zoom(-0.1f);
m_bestFit = false;
}
void ImageScrollAreaGL::bestFit()
{
m_bestFit = true;
m_scale = std::min((float)m_imageWidget->width()/m_imgWidth, (float)m_imageWidget->height()/m_imgHeight);
zoom(0.0f);
}
void ImageScrollAreaGL::oneToOne()
{
m_scale = 1.0f;
zoom(0.0f);
m_bestFit = false;
}
void ImageScrollAreaGL::scrollEvent()
{
m_imageWidget->setOffset(m_horizontalScrollBar->value(), m_verticalScrollBar->value());
}
-96
View File
@@ -1,96 +0,0 @@
#ifndef IMAGESCROLLAREAGL_H
#define IMAGESCROLLAREAGL_H
#include <memory>
#include <QObject>
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_2_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLTexture>
#include <QOpenGLVertexArrayObject>
#include <QScrollBar>
#include "rawimage.h"
typedef enum
{
Linear,
Log,
Sqrt,
Power,
Asinh,
}StretchFunc;
class ImageWidget : public QOpenGLWidget
{
Q_OBJECT
QOpenGLFunctions *f;
std::unique_ptr<QOpenGLShaderProgram> m_program;
std::unique_ptr<QOpenGLBuffer> m_buffer;
std::unique_ptr<QOpenGLTexture> m_image;
std::unique_ptr<QOpenGLVertexArrayObject> m_vao;
std::unique_ptr<QOpenGLPixelTransferOptions> m_transferOptions;
int m_width, m_height;
int m_imgWidth, m_imgHeight;
float m_low;
float m_mid;
float m_high;
float m_range;
float m_dx, m_dy;
float m_scale;
bool m_blockRepaint;
bool m_bwImg;
bool m_invert;
bool m_superpixel;
public:
explicit ImageWidget(QWidget *parent = nullptr);
~ImageWidget();
void setImage(const RawImage *image);
void setImage(const QPixmap &pixmap);
void setScale(float scale);
void blockRepaint(bool block);
public slots:
void setMTFParams(float low, float mid, float high);
void setOffset(int dx, int dy);
void superPixel(bool enable);
void invert(bool enable);
QImage renderToImage();
protected:
void paintGL();
void resizeGL(int w, int h);
void initializeGL();
};
class ImageScrollAreaGL : public QWidget
{
Q_OBJECT
QScrollBar *m_verticalScrollBar;
QScrollBar *m_horizontalScrollBar;
ImageWidget *m_imageWidget;
int m_imgWidth, m_imgHeight;
QPoint m_lastPos;
float m_scale;
bool m_bestFit;
public:
explicit ImageScrollAreaGL(QWidget *parent = nullptr);
~ImageScrollAreaGL();
void setImage(RawImage *image);
void setImage(const QPixmap &pixmap);
ImageWidget* imageWidget();
protected:
void updateScrollbars(bool zoom = false);
void resizeEvent(QResizeEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
void zoom(float delta);
public slots:
void zoomIn();
void zoomOut();
void bestFit();
void oneToOne();
protected slots:
void scrollEvent();
};
#endif // IMAGESCROLLAREAGL_H
+7
View File
@@ -0,0 +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})
Submodule
+1
Submodule libXISF added at 2e74d94641
-369
View File
@@ -1,369 +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 <libexif/exif-data.h>
#include <fitsio2.h>
#include "rawimage.h"
#include "starfit.h"
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level) :
m_file(file),
m_receiver(receiver),
m_analyzeLevel(level)
{
}
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(QString path, ImageInfoData &info, RawImage **image)
{
if(!image)
return false;
LibRaw raw;
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;
if(image)
{
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 = new RawImage(rawdata.sizes.width, rawdata.sizes.height, 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.append(StringPair(QObject::tr("Width"), QString::number(rawImg->width)));
//info.append(StringPair(QObject::tr("Height"), QString::number(rawImg->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 nexist;
int status = 0;
char key[FLEN_KEYWORD];
char val[FLEN_VALUE];
char comm[FLEN_COMMENT];
char strval[FLEN_VALUE];
QVariant var;
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
var = strval;
status = 0;
info.fitsHeader.append({key, var, comm});
}
else
{
return status;
}
}
return status;
}
bool loadFITS(QString path, ImageInfoData &info, RawImage **image)
{
if(!image)
return false;
fitsfile *file;
int status = 0;
int type;
fits_open_image(&file, path.toLocal8Bit().data(), READONLY, &status);
fits_get_hdu_type(file, &type, &status);
if(type == IMAGE_HDU)
{
int imgtype;
int naxis;
long naxes[3] = {0};
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
if(naxis >= 2 && naxis <= 3 && status == 0)
{
int cvtype;
int fitstype;
std::vector<cv::Mat> cvimg;
long fpixel[3] = {1,1,1};
switch(imgtype)
{
case BYTE_IMG:
cvtype = CV_8U;
fitstype = TBYTE;
break;
case SHORT_IMG:
cvtype = CV_16U;
fitstype = TUSHORT;
break;
case FLOAT_IMG:
cvtype = CV_32F;
fitstype = TFLOAT;
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])});
for (int i=1; i==1 || i<=naxes[2]; i++)
{
cv::Mat tmp(h, w, cvtype);
fpixel[2] = i;
fits_read_pix(file, fitstype, fpixel, size, NULL, tmp.ptr(), NULL, &status);
cvimg.push_back(tmp);
}
if(cvimg.size() == 1)
{
*image = new RawImage(cvimg[0]);
}
if(cvimg.size() == 3)
{
cv::Mat rgb;
cv::merge(cvimg, rgb);
*image = new RawImage(rgb);
}
}
}
loadFITSHeader(file, info);
fits_close_file(file, &status);
if(status)
{
char err[100];
fits_get_errstatus(status, err);
info.info.append({QObject::tr("Error"), QString(err)});
qDebug() << err;
}
return true;
}
void LoadRunable::run()
{
if(!m_receiver->isCurrent())
{
return;
}
QElapsedTimer timer;
ImageInfoData info;
QFileInfo finfo(m_file);
info.info.append({QObject::tr("Filename"), finfo.fileName()});
RawImage *rawImage = nullptr;
bool raw = false;
if(m_file.endsWith(".CR2", Qt::CaseInsensitive))
{
timer.start();
loadRAW(m_file, info, &rawImage);
raw = true;
qDebug() << "LoadRaw" << timer.elapsed();
}
else if(m_file.endsWith(".FIT", Qt::CaseInsensitive) || m_file.endsWith(".FITS", Qt::CaseInsensitive))
{
loadFITS(m_file, info, &rawImage);
}
else
{
QImage img(m_file);
ExifData *exif = exif_data_new_from_file(m_file.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 = new RawImage(img);
}
if(rawImage && m_analyzeLevel >= Statistics)
{
double mean, median, min, max, mad;
double stdDev;
timer.start();
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad);
qDebug() << "image stats" << timer.restart();
info.info.append({QObject::tr("Mean"), QString::number(mean)});
info.info.append({QObject::tr("Standart deviation"), QString::number(stdDev)});
info.info.append({QObject::tr("Median"), QString::number(median)});
info.info.append({QObject::tr("Minimum"), QString::number(min)});
info.info.append({QObject::tr("Maximum"), QString::number(max)});
info.info.append({QObject::tr("MAD"), QString::number(mad)});
if(m_analyzeLevel >= Peaks)
{
std::vector<Peak> peaks;
if(raw) {
rawImage->quarter();
qDebug() << "quarter" << timer.restart();
}
RawImage *medianImage = rawImage->medianFilter();
qDebug() << "median" << timer.restart();
int numPeaks = medianImage->findPeaks(median+stdDev*2, 20, peaks);
delete medianImage;
qDebug() << "peaks" << timer.restart();
//if(m_analyzeLevel == Peaks)
// drawPeaks(img, peaks);
qDebug() << "draw peaks" << timer.restart();
info.info.append({QObject::tr("Peaks"), QString::number(numPeaks)});
info.info.append({QObject::tr("Peaks draw"), QString::number(peaks.size())});
if(m_analyzeLevel>= Stars)
{
double fwhmX = 0;
double fwhmY = 0;
const int radius = 13;
StarFit starFit(radius);
std::vector<Star> stars;
for(uint i=0; i<peaks.size(); i++)
{
Peak p = peaks[i];
std::vector<double> r;
int x = p.x();
int y = p.y();
rawImage->rect(x, y, radius, radius, r);
Star star = starFit.fitStar(r, false);
if(star.valid())
{
//printStarModel(radius, r, star);
star.m_x += x;
star.m_y += y;
fwhmX += star.fwhmX();
fwhmY += star.fwhmY();
stars.push_back(star);
}
}
//drawStars(img, stars);
info.info.append({QObject::tr("FWHM X"), QString::number(fwhmX/stars.size())});
info.info.append({QObject::tr("FWHM Y"), QString::number(fwhmY/stars.size())});
}
qDebug() << "Star fit" << timer.restart();
}
}
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(void*, rawImage), Q_ARG(ImageInfoData, info));
}
bool readFITSHeader(const QString &path, ImageInfoData &info)
{
fitsfile *fr;
int status = 0;
fits_open_diskfile(&fr, path.toLocal8Bit().data(), READONLY, &status);
status = loadFITSHeader(fr, info);
fits_close_file(fr, &status);
return status == 0;
}
-22
View File
@@ -1,22 +0,0 @@
#ifndef LOADRUNABLE_H
#define LOADRUNABLE_H
#include <QRunnable>
#include <QString>
#include "imageinfo.h"
bool readFITSHeader(const QString &path, ImageInfoData &info);
class Image;
class LoadRunable : public QRunnable
{
QString m_file;
Image *m_receiver;
AnalyzeLevel m_analyzeLevel;
public:
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level);
void run();
};
#endif // LOADRUNABLE_H
-23
View File
@@ -1,23 +0,0 @@
#include "mainwindow.h"
#include <QApplication>
#include <QSurfaceFormat>
int main(int argc, char *argv[])
{
QSurfaceFormat format;
format.setMajorVersion(3);
format.setMinorVersion(2);
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(":/icon.png"));
MainWindow w;
w.show();
return a.exec();
}
-428
View File
@@ -1,428 +0,0 @@
#include "mainwindow.h"
#include <QScrollArea>
#include <QDir>
#include <QKeyEvent>
#include <QMenu>
#include <QMenuBar>
#include <QFileDialog>
#include <QStandardPaths>
#include <QMessageBox>
#include <QProgressDialog>
#include <QDebug>
#include <QDockWidget>
#include <signal.h>
#include <unistd.h>
#include <QSettings>
#include <QCoreApplication>
#ifdef __linux__
#include <sys/ioctl.h>
#include <linux/btrfs.h>
#include <sys/socket.h>
#endif
int MainWindow::socketPair[2] = {0, 0};
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
qRegisterMetaType<ImageInfoData>("ImageInfoData");
qRegisterMetaType<RawImage*>("RawImage");
m_info = new ImageInfo(this);
QDockWidget *infoDock = new QDockWidget(tr("Image info"), this);
infoDock->setWidget(m_info);
infoDock->setObjectName("infoDock");
addDockWidget(Qt::LeftDockWidgetArea, infoDock);
//m_image = new ImageScrollArea(this);
//m_image->resize(0,0);
//setCentralWidget(m_image);
resize(800, 600);
m_imageGL = new ImageScrollAreaGL(this);
setCentralWidget(m_imageGL);
m_stretchPanel = new StretchPanel(this);
connect(m_stretchPanel, SIGNAL(paramChanged(float,float,float)), m_imageGL->imageWidget(), SLOT(setMTFParams(float,float,float)));
connect(m_stretchPanel, &StretchPanel::autoStretch, [&](){ m_stretchPanel->stretchImage(m_ringList->currentImage().get()); });
connect(m_stretchPanel, &StretchPanel::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
connect(m_stretchPanel, &StretchPanel::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
m_ringList = new ImageRingList(this);
m_filesystem = new FilesystemWidget(m_ringList, this);
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
m_database = new Database(this);
if(!m_database->init())
QMessageBox::critical(this, tr("Can't open DB"), tr("Can't open SQLITE database"));
m_databaseView = new DataBaseView(m_database, this);
connect(m_databaseView, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
QDockWidget *stretchDock = new QDockWidget(tr("Stretch"), this);
stretchDock->setWidget(m_stretchPanel);
stretchDock->setObjectName("strechDock");
addDockWidget(Qt::TopDockWidgetArea, stretchDock);
QDockWidget *filesystemDock = new QDockWidget(tr("Filesystem"), this);
filesystemDock->setWidget(m_filesystem);
filesystemDock->setObjectName("filesystemDock");
addDockWidget(Qt::LeftDockWidgetArea, filesystemDock);
QDockWidget *databaseViewDock = new QDockWidget(tr("FITS files database"), this);
databaseViewDock->setWidget(m_databaseView);
databaseViewDock->setObjectName("databaseViewDock");
databaseViewDock->hide();
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
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)));
QMenu *fileMenu = new QMenu(tr("File"), this);
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence("Ctrl+O"));
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()));
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence("Ctrl+S"));
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
liveModeAction->setCheckable(true);
fileMenu->addAction(tr("Exit"), this, SLOT(close()));
menuBar()->addMenu(fileMenu);
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("Fullscreen"), this, SLOT(toggleFullScreen()), QKeySequence::FullScreen);
menuBar()->addMenu(viewMenu);
QMenu *selectMenu = new QMenu(tr("Select"), this);
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F5);
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
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);
menuBar()->addMenu(selectMenu);
QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
QActionGroup *analyzeGroup = new QActionGroup(this);
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
static QAction* lastAction = nullptr;
if(action == lastAction)
{
action->setChecked(false);
lastAction = nullptr;
}
else
lastAction = action;
});
QAction *statsAction = analyzeGroup->addAction(tr("Image statistics"));
QAction *peakAction = analyzeGroup->addAction(tr("Peak finder"));
QAction *starAction = analyzeGroup->addAction("Star finder");
statsAction->setCheckable(true);
peakAction->setCheckable(true);
starAction->setCheckable(true);
connect(statsAction, SIGNAL(toggled(bool)), this, SLOT(imageStats(bool)));
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);
QMenu *dockMenu = new QMenu(tr("Docks"), this);
dockMenu->addAction(infoDock->toggleViewAction());
dockMenu->addAction(stretchDock->toggleViewAction());
dockMenu->addAction(filesystemDock->toggleViewAction());
dockMenu->addAction(databaseViewDock->toggleViewAction());
menuBar()->addMenu(dockMenu);
setupSigterm();
QSettings settings;
restoreGeometry(settings.value("mainwindow/geometry").toByteArray());
restoreState(settings.value("mainwindow/state").toByteArray());
QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
if(standardLocations.size())
_lastDir = standardLocations.first();
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
m_filesystem->setDir(_lastDir);
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();
}
MainWindow::~MainWindow()
{
delete m_database;
}
void MainWindow::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_Left:
m_ringList->decrement();
break;
case Qt::Key_Right:
m_ringList->increment();
break;
default:
event->ignore();
break;
}
if(event->isAccepted())
updateWindowTitle();
}
void MainWindow::keyReleaseEvent(QKeyEvent *event)
{
event->ignore();
}
void MainWindow::setupSigterm()
{
#ifdef __linux__
struct sigaction signal;
signal.sa_handler = MainWindow::signalHandler;
sigemptyset(&signal.sa_mask);
signal.sa_flags = 0;
signal.sa_flags |= SA_RESTART;
sigaction(SIGHUP, &signal, 0);
sigaction(SIGTERM, &signal, 0);
sigaction(SIGINT, &signal, 0);
::socketpair(AF_UNIX, SOCK_STREAM, 0, socketPair);
socketNotifier = new QSocketNotifier(socketPair[1], QSocketNotifier::Read, this);
connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(socketNotify()));
#endif
}
void MainWindow::signalHandler(int)
{
char a = 1;
::write(socketPair[0], &a, sizeof(a));
}
void MainWindow::closeEvent(QCloseEvent *event)
{
QSettings settings;
settings.setValue("mainwindow/geometry", saveGeometry());
settings.setValue("mainwindow/state", saveState());
QMainWindow::closeEvent(event);
}
void MainWindow::socketNotify()
{
socketNotifier->setEnabled(false);
char tmp;
read(socketPair[1], &tmp, sizeof(tmp));
close();
socketNotifier->setEnabled(true);
}
void MainWindow::pixmapLoaded(Image *image)
{
//m_image->setImage(image->pixmap());
if(image->rawImage())
{
m_imageGL->setImage(image->rawImage());
}
}
void MainWindow::loadFile()
{
QString file = QFileDialog::getOpenFileName(this, tr("Open file"), _lastDir, tr("Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)"));
loadFile(file);
}
void MainWindow::loadFile(const QString path)
{
if(!path.isEmpty())
{
QFileInfo info(path);
m_ringList->setFile(info.canonicalFilePath());
updateWindowTitle();
if(info.isDir())
_lastDir = info.absolutePath();
else
_lastDir = info.canonicalPath();
QSettings settings;
settings.setValue("mainwindow/lastdir", _lastDir);
m_filesystem->setDir(_lastDir);
}
}
void MainWindow::loadFile(int row)
{
m_ringList->loadFile(row);
}
void MainWindow::indexDir()
{
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir);
if(!dir.isEmpty())
{
QProgressDialog progressDialog(tr("Indexing FITS files"), tr("Cancel"), 0, 1, this);
progressDialog.setModal(true);
m_database->indexDir(dir, &progressDialog);
}
}
void MainWindow::saveAs()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save as"), _lastDir, tr("Images (*.jpg *.png *.JPG *.PNG)"));
if(!file.isEmpty())
{
QImage img = m_imageGL->imageWidget()->renderToImage();
if(!img.isNull())
img.save(file);
}
}
void MainWindow::markImage()
{
ImagePtr ptr = m_ringList->currentImage();
if(ptr)
{
QString file = ptr->name();
if(!file.isEmpty())
m_database->mark(file);
updateWindowTitle();
}
}
void MainWindow::unmarkImage()
{
ImagePtr ptr = m_ringList->currentImage();
if(ptr)
{
QString file = ptr->name();
if(!file.isEmpty())
m_database->unmark(file);
updateWindowTitle();
}
}
void MainWindow::markAndNext()
{
markImage();
m_ringList->increment();
updateWindowTitle();
}
void MainWindow::unmarkAndNext()
{
unmarkImage();
m_ringList->increment();
updateWindowTitle();
}
void MainWindow::copyMarked()
{
QString dest = QFileDialog::getExistingDirectory(this, tr("Select destination"));
QDir dir(dest);
if(!dest.isEmpty() && dir.exists())
{
int i = 0;
QStringList files = m_database->getMarkedFiles();
QProgressDialog progress(tr("Copying"), tr("Cancel"), 0, files.size(), this);
progress.setWindowModality(Qt::WindowModal);
progress.show();
foreach(const QString &file, files)
{
QFileInfo info(file);
QFile srcFile(file);
QFile dstFile(dir.absoluteFilePath(info.fileName()));
if(dstFile.exists())
continue;
if(progress.wasCanceled())
break;
#ifdef __linux__
srcFile.open(QIODevice::ReadOnly);
dstFile.open(QIODevice::WriteOnly);
if(ioctl(dstFile.handle(), BTRFS_IOC_CLONE, srcFile.handle()) < 0)
{
dstFile.remove();
dstFile.close();
qDebug() << dstFile.fileName();
srcFile.copy(dstFile.fileName());
}
#else
srcFile.copy(dstFile.fileName());
#endif
progress.setValue(i++);
}
}
}
void MainWindow::toggleFullScreen()
{
if(isFullScreen())
{
showNormal();
if(_maximized)showMaximized();
}
else
{
_maximized = isMaximized();
showFullScreen();
}
}
void MainWindow::liveMode(bool active)
{
m_ringList->setLiveMode(active);
}
void MainWindow::imageStats(bool imageStats)
{
m_ringList->setCalculateStats(imageStats);
}
void MainWindow::peakFinder(bool findPeaks)
{
m_ringList->setFindPeaks(findPeaks);
}
void MainWindow::starFinder(bool findStars)
{
m_ringList->setFindStars(findStars);
}
void MainWindow::updateWindowTitle()
{
ImagePtr ptr = m_ringList->currentImage();
if(ptr)
{
QFileInfo info(ptr->name());
QString title = info.fileName();
if(m_database->isMarked(ptr->name()))
title += " *";
setWindowTitle(title);
}
}
-259
View File
@@ -1,259 +0,0 @@
#include "rawimage.h"
#include <QDebug>
RawImage::ImgType CV2Type(int cvtype)
{
switch (cvtype)
{
case CV_8U:
return RawImage::UINT8;
case CV_16U:
return RawImage::UINT16;
case CV_32F:
return RawImage::FLOAT32;
case CV_8UC3:
return RawImage::UINT8C3;
case CV_8UC4:
return RawImage::UINT8C4;
case CV_16UC3:
return RawImage::UINT16C3;
case CV_32FC3:
return RawImage::FLOAT32C3;
default:
return RawImage::UNKNOWN;
}
}
int Type2CV(RawImage::ImgType type)
{
switch (type)
{
case RawImage::UINT8:
return CV_8U;
case RawImage::UINT16:
return CV_16U;
case RawImage::FLOAT32:
return CV_32F;
case RawImage::UINT8C3:
return CV_8UC3;
case RawImage::UINT8C4:
return CV_8UC4;
case RawImage::UINT16C3:
return CV_16UC3;
case RawImage::FLOAT32C3:
return CV_32FC3;
case RawImage::UNKNOWN:
return CV_8S;
default:
return CV_8U;
}
}
RawImage::RawImage()
{
m_stats = false;
}
RawImage::RawImage(int w, int h, ImgType type)
{
m_img.create(h, w, Type2CV(type));
m_stats = false;
}
RawImage::RawImage(cv::Mat &img)
{
m_img = img;
m_stats = false;
}
RawImage::RawImage(const RawImage &d)
{
d.m_img.copyTo(m_img);
m_mean = d.m_mean;
m_stdDev = d.m_stdDev;
m_median = d.m_median;
m_min = d.m_min;
m_max = d.m_max;
m_mad = d.m_mad;
m_stats = d.m_stats;
}
RawImage::RawImage(const QImage &img)
{
if(img.format() == QImage::Format_RGB32)
{
m_img.create(img.height(), img.width(), CV_8UC4);
for(int i=0; i<img.height(); i++)
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*4);
}
else
{
QImage tmp = img.convertToFormat(QImage::Format_RGB888);
m_img.create(img.height(), img.width(), CV_8UC3);
for(int i=0; i<tmp.height(); i++)
std::memcpy(m_img.ptr(i), tmp.scanLine(i), tmp.width()*3);
}
m_stats = false;
}
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad)
{
if(!m_stats)calcStats();
if(mean)*mean = m_mean;
if(stdDev)*stdDev = m_stdDev;
if(median)*median = m_median;
if(min)*min = m_min;
if(max)*max = m_max;
if(mad)*mad = m_mad;
return true;
}
void RawImage::calcStats()
{
if(m_stats)return;
m_stats = true;
cv::Scalar meanS, stdDevS;
cv::meanStdDev(m_img, meanS, stdDevS);
cv::minMaxIdx(m_img, &m_min, &m_max);
cv::Mat img;
if(m_img.channels() == 1)img = m_img;
else if (m_img.channels() == 3)cv::cvtColor(m_img, img, cv::COLOR_BGR2GRAY);
else if (m_img.channels() == 4)cv::cvtColor(m_img, img, cv::COLOR_BGRA2GRAY);
int histSize = 256;
if(img.type() == CV_16U || img.type() == CV_32F)histSize = 65536;
float range[] = {0, (float)histSize};
if(img.type() == CV_32F)range[1] = 1.0f;
const float *ranges[] = {range};
cv::Mat hist;
cv::calcHist(&img, 1, nullptr, cv::Mat(), hist, 1, &histSize, ranges);
m_mean = meanS[0];
m_stdDev = stdDevS[0];
size_t halfImageSize = size()/2;
size_t medianSum = 0;
for(int i=0; i < histSize; i++)
{
medianSum += hist.at<float>(0, i);
if(medianSum >= halfImageSize)
{
m_median = i;
break;
}
}
cv::Mat absDev;
img.convertTo(absDev, CV_32F, 1, -m_median);
absDev = cv::abs(absDev);
cv::Mat madHist;
medianSum = 0;
cv::calcHist(&absDev, 1, nullptr, cv::Mat(), madHist, 1, &histSize, ranges);
for(int i=0; i < histSize; i++)
{
medianSum += madHist.at<float>(0, i);
if(medianSum >= halfImageSize)
{
m_mad = i;
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 mask, dilate, locMax, result;
cv::dilate(m_img, dilate, kernel);
cv::compare(m_img, dilate, locMax, cv::CMP_GE);
cv::compare(m_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();
}
RawImage* RawImage::medianFilter() const
{
RawImage *ret = new RawImage();
cv::medianBlur(m_img, ret->m_img, 3);
return ret;
}
void RawImage::quarter()
{
}
uint32_t RawImage::width() const
{
return m_img.cols;
}
uint32_t RawImage::height() const
{
return m_img.rows;
}
uint32_t RawImage::size() const
{
return width()*height();
}
RawImage::ImgType RawImage::type() const
{
return CV2Type(m_img.type());
}
uint32_t RawImage::norm() const
{
switch(m_img.type())
{
case CV_8U:
case CV_8UC3:
case CV_8UC4:
return UINT8_MAX;
case CV_16U:
case CV_16UC3:
return UINT16_MAX;
default:
return 1;
}
}
void* RawImage::data()
{
return m_img.ptr();
}
const void *RawImage::data() const
{
return m_img.ptr();
}
-77
View File
@@ -1,77 +0,0 @@
#ifndef RAWIMAGE_H
#define RAWIMAGE_H
#include <vector>
#include <algorithm>
#include <stdint.h>
#include <math.h>
#include <memory.h>
#include <opencv2/imgproc.hpp>
#include <QImage>
class Peak
{
uint32_t m_v;
uint32_t m_x,m_y;
public:
Peak() : m_v(0), m_x(0), m_y(0) {}
Peak(uint32_t v, uint32_t x, uint32_t y) : m_v(v), m_x(x), m_y(y) {}
uint32_t v() const { return m_v; }
uint32_t x() const { return m_x; }
uint32_t y() const { return m_y; }
double distance(const Peak &d) const
{
uint32_t dx = m_x-d.m_x;
uint32_t dy = m_y-d.m_y;
return dx*dx + dy*dy;
}
bool operator <(const Peak &d) const
{
return m_v > d.m_v;
}
};
class RawImage
{
protected:
cv::Mat m_img;
bool m_stats;
double m_mean;
double m_stdDev;
double m_median;
double m_min;
double m_max;
double m_mad;
public:
enum ImgType
{
UINT8,
UINT16,
FLOAT32,
UINT8C3,
UINT8C4,
UINT16C3,
FLOAT32C3,
UNKNOWN,
};
RawImage();
RawImage(int w, int h, ImgType type);
RawImage(cv::Mat &img);
RawImage(const RawImage &d);
RawImage(const QImage &img);
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad);
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;
RawImage* medianFilter() const;
void quarter();
uint32_t width() const;
uint32_t height() const;
uint32_t size() const;
ImgType type() const;
uint32_t norm() const;
void* data();
const void* data() const;
};
#endif // RAWIMAGE_H
-12
View File
@@ -1,12 +0,0 @@
<RCC>
<qresource prefix="/shaders">
<file>image.frag</file>
<file>image.vert</file>
</qresource>
<qresource prefix="/">
<file>icon.png</file>
<file>invert.png</file>
<file>nuke.png</file>
<file>bayer.png</file>
</qresource>
</RCC>
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

+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

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File
View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
View File
Binary file not shown.
View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

+34
View File
@@ -0,0 +1,34 @@
<RCC>
<qresource prefix="/">
<file>invert.png</file>
<file>nuke.png</file>
<file>bayer.png</file>
<file>nuke_a.png</file>
<file>../about/tenmon</file>
<file>../translations/tenmon_en.qm</file>
<file>../translations/tenmon_sk.qm</file>
<file>../about/filter.png</file>
<file>../about/stretch-panel.png</file>
<file>../translations/tenmon_fr.qm</file>
<file>falsecolor.png</file>
<file>link.png</file>
<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>
</qresource>
<qresource lang="sk" prefix="/">
<file alias="help">../about/help_sk</file>
</qresource>
<qresource lang="fr" prefix="/">
<file alias="help">../about/help_fr</file>
</qresource>
</RCC>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="space.nouspiro.tenmon.svg"
inkscape:export-filename="/home/nou/c++/tenmon/space.nouspiro.tenmon_128.png"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008"
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:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
units="px"
width="128px"
inkscape:zoom="2.6547419"
inkscape:cx="54.430903"
inkscape:cy="78.162024"
inkscape:window-width="1862"
inkscape:window-height="1136"
inkscape:window-x="58"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Vrstva 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke:#ffffff;stroke-width:0;stroke-linejoin:round"
id="rect1196"
width="33.866665"
height="33.866665"
x="5e-07"
y="5e-07" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:17.276px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.359917"
x="-0.41414261"
y="23.331123"
id="text8592"><tspan
sodipodi:role="line"
id="tspan8590"
style="fill:#ffffff;stroke-width:0.359917"
x="-0.41414261"
y="23.331123">天文</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.
+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>
+40
View File
@@ -0,0 +1,40 @@
uniform sampler2D qt_Texture0;
uniform ivec2 firstRed;
in vec2 qt_TexCoord0;
in vec2 center;
layout(location = 0) out vec4 color;
#define f(x, y) texelFetch(qt_Texture0, icenter + ivec2(x, y), 0).r
void main(void)
{
ivec2 texSize = textureSize(qt_Texture0, 0);
ivec2 icenter = ivec2(center);
ivec2 alternate = (icenter + firstRed) % 2;
// cross, checker, theta, phi
const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0;
const vec4 kB = vec4( 2.0, 0.0, 0.0, 4.0) / 8.0;
const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0;
const vec4 kD = vec4( 0.0, 2.0, -1.0, -1.0) / 8.0;
const vec4 kE = vec4(-1.0, -1.5, -1.0, 0.5) / 8.0;
const vec4 kF = vec4( 2.0, 0.0, 4.0, 0.0) / 8.0;
float A = f(0,2) + f(0,-2);
float B = f(0,1) + f(0,-1);
float C = f(0,0);
float D = f(1,1) + f(-1,1) + f(1,-1) + f(-1,-1);
float E = f(2,0) + f(-2,0);
float F = f(1,0) + f(-1,0);
vec4 P = kA*A + kB*B + kC*C + kD*D + kE*E + kF*F;
color.rgb = alternate.y == 0 ?
(alternate.x == 0 ? vec3(C, P.xy) : // even row even col
vec3(P.z, C, P.w)) : // even row odd col
(alternate.x == 0 ? vec3(P.w, C, P.z) : // odd row even col
vec3(P.yx, C)); // odd row odd col
color.a = 1.0;
}
+14
View File
@@ -0,0 +1,14 @@
uniform sampler2D qt_Texture0;
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
out vec2 qt_TexCoord0;
out vec2 center;
void main(void)
{
vec2 texSize = vec2(textureSize(qt_Texture0, 0));
gl_Position = vec4(qt_Vertex, 0.0, 1.0);
qt_TexCoord0 = qt_MultiTexCoord0;
qt_TexCoord0.y = 1.0 - qt_TexCoord0.y;
center = qt_TexCoord0 * texSize.xy;
}
+162
View File
@@ -0,0 +1,162 @@
uniform sampler2D qt_Texture0;
uniform sampler3D lut_table;
uniform sampler2DArray colormap;
uniform vec3 mtf_param[3];
uniform vec2 unit_scale;
uniform bool bw;
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;
vec3 Linear2sRGB(vec3 color)
{
return mix(12.92 * color.rgb,
1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,
greaterThan(color, vec3(0.0031308)));
}
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.0) * x) / ((2.0 * mid - 1.0) * x - mid);
}
vec3 falsecolor(float color)
{
color *= 255.0 / 256.0;
color += 0.5 / 256.0;
return texture(colormap, vec3(color, 0.5, colormapIdx)).rgb;
}
vec3 checker()
{
vec2 pattern = fract(gl_FragCoord.xy * 0.0625) - 0.5;
return vec3(step(pattern.x * pattern.y, 0.0) * 0.25 + 0.25);
}
vec4 cubic(float v)
{
vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
vec4 s = n * n * n;
float x = s.x;
float y = s.y - 4.0 * s.x;
float z = s.z - 4.0 * s.y + 6.0 * s.x;
float w = 6.0 - x - y - z;
return vec4(x, y, z, w) * (1.0/6.0);
}
vec4 textureBicubic(sampler2D sampler, vec2 texCoords)
{
vec2 texSize = vec2(textureSize(sampler, 0));
vec2 invTexSize = 1.0 / texSize;
texCoords = texCoords * texSize - 0.5;
vec2 fxy = fract(texCoords);
texCoords -= fxy;
vec4 xcubic = cubic(fxy.x);
vec4 ycubic = cubic(fxy.y);
vec4 c = texCoords.xxyy + vec2 (-0.5, +1.5).xyxy;
vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
vec4 offset = c + vec4 (xcubic.yw, ycubic.yw) / s;
offset *= invTexSize.xxyy;
vec4 sample0 = texture(sampler, offset.xz);
vec4 sample1 = texture(sampler, offset.yz);
vec4 sample2 = texture(sampler, offset.xw);
vec4 sample3 = texture(sampler, offset.yw);
float sx = s.x / (s.x + s.y);
float sy = s.z / (s.z + s.w);
return mix(mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy);
}
vec4 textureCatmul(sampler2D sampler, vec2 texCoords)
{
ivec2 texSize = textureSize(sampler, 0);
texCoords = texCoords * vec2(texSize) - 0.5;
ivec2 texel = ivec2(floor(texCoords));
vec2 fra = fract(texCoords);
texSize -= 1;
const mat4 CatMul = mat4(0, 1, 0, 0, -0.5, 0, 0.5, 0, 1, -2.5, 2, -0.5, -0.5, 1.5, -1.5, 0.5);
vec4 xx = CatMul * vec4(1.0, fra.x, fra.x*fra.x, fra.x*fra.x*fra.x);
vec4 yy = CatMul * vec4(1.0, fra.y, fra.y*fra.y, fra.y*fra.y*fra.y);
vec4 a00 = texelFetch(sampler, clamp(texel + ivec2(-1, -1), ivec2(0, 0), texSize), 0) * xx.x;
vec4 a01 = texelFetch(sampler, clamp(texel + ivec2( 0, -1), ivec2(0, 0), texSize), 0) * xx.y;
vec4 a02 = texelFetch(sampler, clamp(texel + ivec2( 1, -1), ivec2(0, 0), texSize), 0) * xx.z;
vec4 a03 = texelFetch(sampler, clamp(texel + ivec2( 2, -1), ivec2(0, 0), texSize), 0) * xx.w;
vec4 a10 = texelFetch(sampler, clamp(texel + ivec2(-1, 0), ivec2(0, 0), texSize), 0) * xx.x;
vec4 a11 = texelFetch(sampler, clamp(texel + ivec2( 0, 0), ivec2(0, 0), texSize), 0) * xx.y;
vec4 a12 = texelFetch(sampler, clamp(texel + ivec2( 1, 0), ivec2(0, 0), texSize), 0) * xx.z;
vec4 a13 = texelFetch(sampler, clamp(texel + ivec2( 2, 0), ivec2(0, 0), texSize), 0) * xx.w;
vec4 a20 = texelFetch(sampler, clamp(texel + ivec2(-1, 1), ivec2(0, 0), texSize), 0) * xx.x;
vec4 a21 = texelFetch(sampler, clamp(texel + ivec2( 0, 1), ivec2(0, 0), texSize), 0) * xx.y;
vec4 a22 = texelFetch(sampler, clamp(texel + ivec2( 1, 1), ivec2(0, 0), texSize), 0) * xx.z;
vec4 a23 = texelFetch(sampler, clamp(texel + ivec2( 2, 1), ivec2(0, 0), texSize), 0) * xx.w;
vec4 a30 = texelFetch(sampler, clamp(texel + ivec2(-1, 2), ivec2(0, 0), texSize), 0) * xx.x;
vec4 a31 = texelFetch(sampler, clamp(texel + ivec2( 0, 2), ivec2(0, 0), texSize), 0) * xx.y;
vec4 a32 = texelFetch(sampler, clamp(texel + ivec2( 1, 2), ivec2(0, 0), texSize), 0) * xx.z;
vec4 a33 = texelFetch(sampler, clamp(texel + ivec2( 2, 2), ivec2(0, 0), texSize), 0) * xx.w;
vec4 c = vec4(0.0);
c += (a00 + a01 + a02 + a03) * yy.x;
c += (a10 + a11 + a12 + a13) * yy.y;
c += (a20 + a21 + a22 + a23) * yy.z;
c += (a30 + a31 + a32 + a33) * yy.w;
return c;
}
void main(void)
{
switch(filtering)
{
case 0://nearest
color = texelFetch(qt_Texture0, ivec2(qt_TexCoord0 * vec2(textureSize(qt_Texture0, 0))), 0);
break;
default:
case 1://bilinear
color = texture(qt_Texture0, qt_TexCoord0);
break;
case 2://catmul bicubic
color = textureCatmul(qt_Texture0, qt_TexCoord0);
break;
}
color.rgb = color.rgb * unit_scale.x + unit_scale.y;
if(bw)color = color.rrra;
color = MTF(color, vec4(mtf_param[0], 0.0), vec4(mtf_param[1], 0.5), vec4(mtf_param[2], 1.0));
if(false_color)color.rgb = falsecolor(color.r);
if(invert)color.rgb = vec3(1.0) - color.rgb;
color.rgb = mix(checker(), color.rgb, color.a);
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);
color.a = 1.0;
}
-2
View File
@@ -1,5 +1,3 @@
#version 130
uniform sampler2D qt_Texture0;
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
+10
View File
@@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/">
<file>debayer.frag</file>
<file>debayer.vert</file>
<file>image.frag</file>
<file>image.vert</file>
<file>thumb.frag</file>
<file>thumb.vert</file>
</qresource>
</RCC>
+20
View File
@@ -0,0 +1,20 @@
uniform sampler2DArray qt_Texture0;
uniform vec3 mtf_param[3];
uniform bool invert;
in vec3 qt_TexCoord0;
layout(location = 0) out vec4 color;
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.0) * x) / ((2.0 * mid - 1.0) * x - mid);
}
void main(void)
{
color = texture(qt_Texture0, qt_TexCoord0);
color = MTF(color, vec4(mtf_param[0], 0.0), vec4(mtf_param[1], 0.5), vec4(mtf_param[2], 1.0));
if(invert)color = vec4(1.0) - color;
color.a = 1.0;
}
+19
View File
@@ -0,0 +1,19 @@
in vec2 qt_Vertex;
in vec2 qt_MultiTexCoord0;
in ivec3 imageSize_num;
out vec3 qt_TexCoord0;
uniform ivec3 viewport_row;
uniform mat4 mvp;
uniform vec2 offset;
uniform ivec3 thumb_size;
void main(void)
{
vec2 pos = qt_Vertex * 0.5;
pos.y *= -1.0;
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 + vec2(off), 0.0, 1.0);
qt_TexCoord0 = vec3(qt_MultiTexCoord0, float(imageSize_num.z) + 0.1);
}
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Exec=tenmon %U
Icon=space.nouspiro.tenmon
Comment=FITS Image viewer
Name=Tenmon
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;Science;Astronomy
MimeType=image/fits;image/x-xisf;
Terminal=false
+230
View File
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>space.nouspiro.tenmon</id>
<launchable type="desktop-id">space.nouspiro.tenmon.desktop</launchable>
<name>Tenmon</name>
<summary>FITS/XISF image viewer, converter, index and search</summary>
<developer id="nouspiro.space">
<name>Dušan Poizl</name>
</developer>
<developer_name>Dušan Poizl</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<description>
<p>It 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>RAW CR2/CR3, DNG, NEF</li>
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
</ul>
<p>Features of application:</p>
<ul>
<li>Using same stretch function as PixInsight</li>
<li>OpenGL accelerated drawing</li>
<li>Index and search FITS XISF header data</li>
<li>Quick mark images and then copy/move marked files</li>
<li>Convert FITS &lt;-&gt; XISF</li>
<li>Convert FITS/XISF -&gt; JPEG/PNG</li>
<li>Image statistics mean, media, min, max</li>
<li>Support for WCS</li>
<li>Thumbnails</li>
<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>
<category>Graphics</category>
<category>Viewer</category>
<category>Science</category>
<category>Astronomy</category>
</categories>
<keywords>
<keyword>astronomy</keyword>
</keywords>
<url type="homepage">https://nouspiro.space/?page_id=206</url>
<url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url>
<screenshots>
<screenshot type="default">
<caption>Main window with image</caption>
<image>https://nouspiro.space/wp-content/uploads/2022/04/tenmon-1024x579.png</image>
</screenshot>
<screenshot type="default">
<caption>Thumnail view</caption>
<image>https://nouspiro.space/wp-content/uploads/2022/12/tenmon2-1024x645.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>
<releases>
<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>
<li>Opening directory recursively</li>
</ul>
</description>
</release>
<release version="20240201" date="2024-02-01">
<description>
<ul>
<li>Smooth thumbnails</li>
<li>Respect ROWORDER</li>
<li>Bugfixes</li>
</ul>
</description>
</release>
<release version="20240108" date="2024-01-08">
<description>
<ul>
<li>Update to Qt6</li>
<li>Add support for CR3 RAW files</li>
<li>Slideshow</li>
<li>Improved rapid image view</li>
</ul>
</description>
</release>
<release version="20231116" date="2023-11-16">
<description>
<ul>
<li>Histogram</li>
<li>False colors</li>
<li>Strech each RGB channel individually</li>
<li>Better white balancing</li>
</ul>
</description>
</release>
<release version="20230419" date="2023-04-19">
<description>
<ul>
<li>Add file sorting</li>
<li>Improved zoom and scrolling.</li>
<li>Shift modify if zoom on mouse position or center</li>
<li>Fix issue with XISF from NINA</li>
<li>Fix crash in flatpak version</li>
</ul>
</description>
</release>
<release version="20230212" date="2023-02-12">
<description>
<ul>
<li>Replace PCL with LibXISF</li>
<li>Fix issue with OpenGL</li>
<li>Fix loading SATURATION settings</li>
<li>Fix issue with whitebalance coefficients</li>
</ul>
</description>
</release>
<release version="20221228" date="2022-12-28">
<description>
<ul>
<li>Add gray world white balance</li>
<li>Export database to CSV</li>
<li>Fine tune of STF when Shift key is pressed</li>
<li>Add saturation statistic</li>
<li>Various fixes</li>
</ul>
</description>
</release>
<release version="20221215" date="2022-12-15">
<description>
<ul>
<li>Add debayer support</li>
<li>Move to trash action</li>
<li>Change copy and move shortcuts to F5 and F6</li>
<li>Change mark and unmark key shortcuts to F7 and F8</li>
<li>Fix not refreshing file tree</li>
<li>Add option to not use native file dialog</li>
</ul>
</description>
</release>
<release version="20221209" date="2022-12-09"/>
<release version="20221126" date="2022-11-26"/>
<release version="20221121" date="2022-11-11"/>
<release version="20221023" date="2022-10-23"/>
</releases>
</component>
+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>
+53
View File
@@ -0,0 +1,53 @@
#include "about.h"
#include <QTextEdit>
#include <QLabel>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QFile>
#include <QLocale>
#include "gitversion.h"
About::About(QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("About Tenmon"));
QVBoxLayout *layout = new QVBoxLayout(this);
QLabel *label = new QLabel(this);
QFile tenmonText(":/about/tenmon");
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);
layout->addWidget(label);
layout->addWidget(buttonBox);
}
HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Help"));
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);
helpText->setHtml(tenmonText.readAll());
}
QString getVersion()
{
QString version = GITVERSION;
version.truncate(8);
return version;
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef ABOUT_H
#define ABOUT_H
#include <QDialog>
class About : public QDialog
{
Q_OBJECT
public:
About(QWidget *parent = nullptr);
};
class HelpDialog : public QDialog
{
Q_OBJECT
public:
HelpDialog(QWidget *parent = nullptr);
};
QString getVersion();
#endif // ABOUT_H
+426
View File
@@ -0,0 +1,426 @@
#include "batchprocessing.h"
#include "ui_batchprocessing.h"
#include <functional>
#include <QDir>
#include <QFileDialog>
#include <QStandardPaths>
#include <QProcess>
#include <QSettings>
#include <QCloseEvent>
#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
QList<QPair<QString, QString>> scanDirectories(const QStringList &paths)
{
QList<QPair<QString, QString>> files;
QStringList scannedDirs;
std::function<void(const QString &root, const QString &path)> scanDirectory = [&](const QString &root, const QString &path)
{
QFileInfo info(path);
if(info.isDir() && !scannedDirs.contains(info.canonicalFilePath()))
{
scannedDirs.append(info.canonicalFilePath());
QDir dir(path);
QStringList entries = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for(QString &entry : entries)
scanDirectory(root, dir.absoluteFilePath(entry));
}
else if(info.isFile())
{
if(path == root)
files.append({path, info.absolutePath()});
else
files.append({path, root});
}
};
for(const QString &path : paths)
scanDirectory(path, path);
return files;
}
void BatchProcessing::scanScriptDir()
{
QString current;
if(_ui->scriptsList->currentItem())
current = _ui->scriptsList->currentItem()->text();
_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(Database *database, QWidget *parent) : QDialog(parent)
, _database(database)
{
_ui = new Ui::BatchProcessing;
_ui->setupUi(this);
QStringList scriptsPath = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
if(scriptsPath.size())
{
QDir dir(scriptsPath.first());
if(!dir.exists("scripts"))
{
if(!dir.mkpath("scripts"))
qWarning() << "Failed to create scripts directory";
}
dir.cd("scripts");
_scriptBasePath = dir.absolutePath() + "/";
scanScriptDir();
_fileWatcher.addPath(_scriptBasePath);
connect(&_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &BatchProcessing::scanScriptDir);
}
else
{
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->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->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());
}
BatchProcessing::~BatchProcessing()
{
delete _engineThread;
QSettings settings;
settings.setValue("batchprocessing/outputpath", _ui->outputPath->text());
delete _ui;
}
void BatchProcessing::closeEvent(QCloseEvent *event)
{
if(_engineThread)
{
QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Interrupt running script?"), tr("Interrupt running script?"));
if(ret == QMessageBox::StandardButton::Yes)
{
_engineThread->interrupt();
event->accept();
}
else
{
event->ignore();
}
}
else
{
event->accept();
}
}
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());
}
void BatchProcessing::addFiles()
{
QSettings settings;
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
if(!files.isEmpty())
{
_ui->pathsList->addItems(files);
settings.setValue("batchprocessing/inputpath", QFileInfo(files.first()).absolutePath());
}
refreshPaths();
}
void BatchProcessing::addDir()
{
QSettings settings;
QString dir = QFileDialog::getExistingDirectory(this, tr("Select directory"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
if(!dir.isEmpty())
{
_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"), _ui->outputPath->text());
if(!output.isEmpty())
_ui->outputPath->setText(output);
}
void BatchProcessing::openScriptDir()
{
openDir(_scriptBasePath);
}
void BatchProcessing::runScript()
{
_ui->log->clear();
auto selectedItems = _ui->scriptsList->selectedItems();
if(selectedItems.size())
{
_engineThread = new Script::ScriptEngineThread(_database, this);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
QFileInfo outDir(_ui->outputPath->text());
if(outDir.exists() && outDir.isWritable())
{
QString script = selectedItems.first()->text();
if(QDir(_scriptBasePath).exists(script))
script = _scriptBasePath + script;
else
script = ":/scripts/" + script;
_engineThread->setParams(script, _paths, _ui->outputPath->text());
_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"));
}
}
}
void BatchProcessing::stopScript()
{
qDebug() << "Stop script";
if(_engineThread)
_engineThread->interrupt();
}
void BatchProcessing::scriptFinished()
{
_ui->startButton->setEnabled(true);
_ui->stopButton->setEnabled(false);
qDebug() << "script finished";
_engineThread->deleteLater();
_engineThread = nullptr;
}
void BatchProcessing::newMessage(const QString &message, bool error)
{
if(error)_ui->log->setTextColor(Qt::red);
else _ui->log->setTextColor(_textColor);
_ui->log->append(message);
}
QJSValue BatchProcessing::getString(const QString &label, const QString &text)
{
bool ok = false;
QString ret = QInputDialog::getText(this, tr("Enter text"), label, QLineEdit::Normal, text, &ok);
return ok ? ret : QJSValue();
}
QJSValue BatchProcessing::getInt(const QString &label, int value)
{
bool ok = false;
int ret = QInputDialog::getInt(this, tr("Enter integer number"), label, value, INT_MIN, INT_MAX, 1, &ok);
return ok ? ret : QJSValue();
}
QJSValue BatchProcessing::getFloat(const QString &label, double value, int decimals)
{
bool ok = false;
double ret = QInputDialog::getDouble(this, tr("Enter float number"), label, value, -INFINITY, INFINITY, decimals, &ok);
return ok ? ret : QJSValue();
}
QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label, int current)
{
bool ok = false;
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
}
+72
View File
@@ -0,0 +1,72 @@
#ifndef BATCHPROCESSING_H
#define BATCHPROCESSING_H
#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
Ui::BatchProcessing *_ui;
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(Database *database, QWidget *parent = nullptr);
~BatchProcessing();
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 stopScript();
void scriptFinished();
void newMessage(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
+263
View File
@@ -0,0 +1,263 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BatchProcessing</class>
<widget class="QDialog" name="BatchProcessing">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1024</width>
<height>768</height>
</rect>
</property>
<property name="windowTitle">
<string>Batch Processing</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Input files and directories</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="pathsList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="addFilesButton">
<property name="text">
<string>Add files</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addDirButton">
<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>
<widget class="QPushButton" name="removeButton">
<property name="text">
<string>Remove</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeAllButton">
<property name="text">
<string>Remove all</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Output directory</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="outputPath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="text">
<string>Browse</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Scripts</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="openScriptsButton">
<property name="text">
<string>Open scripts</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="scriptsList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Log</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="log">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>4</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>FreeMono</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="ConsoleLine" name="consoleLineEdit">
<property name="placeholderText">
<string>Console</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="executeButton">
<property name="text">
<string>Execute</string>
</property>
<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>
<widget class="QPushButton" name="stopButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Stop script</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</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/>
</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
+412
View File
@@ -0,0 +1,412 @@
#include "database.h"
#include <QStandardPaths>
#include <QDir>
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
#include "loadimage.h"
Database::Database(QObject *parent) : QObject(parent)
{
}
bool Database::init(const QLatin1String &connectionName)
{
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(path);
database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
ngc = QSqlDatabase::addDatabase("QSQLITE", connectionName + "ngc");
if(!dir.mkpath("."))
return false;
if(ngc.isValid())
{
QString ngcDb = dir.absoluteFilePath("ngc.db");
if(!QFile::exists(ngcDb))
QFile::copy(":/ngc.db", ngcDb);
ngc.setDatabaseName(ngcDb);
if(ngc.open())
{
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
{
qDebug() << "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)
{
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)");
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)");
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)");
}
else if(version > 1)
{
qDebug() << "Database version is too new";
return false;
}
QSqlError error = database.lastError();
if(error.type() == QSqlError::NoError)
{
m_markQuery = QSqlQuery(database);
m_markQuery.prepare("INSERT INTO files (file) VALUES (?)");
m_unmarkQuery = QSqlQuery(database);
m_unmarkQuery.prepare("DELETE FROM files WHERE file = (?)");
m_isMarkedQuery = QSqlQuery(database);
m_isMarkedQuery.prepare("SELECT * FROM files WHERE file = (:name)");
m_insertFile = QSqlQuery(database);
m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
m_insertFileWcs = QSqlQuery(database);
m_insertFileWcs.prepare("INSERT INTO fits_files (file, mtime, minRa, maxRa, minDec, maxDec, crVal1, crVal2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
m_insertFitsHeader = QSqlQuery(database);
m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
m_checkFile = QSqlQuery(database);
m_checkFile.prepare("SELECT id,mtime FROM fits_files WHERE file=?");
m_headerKeywords = QSqlQuery(database);
m_headerKeywords.prepare("SELECT DISTINCT key FROM fits_headers ORDER BY key");
m_deleteFile = QSqlQuery(database);
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
return true;
}
qDebug() << error.text();
}
else
{
qDebug() << "Failed to open database" << connectionName;
}
}
else
{
qDebug() << "Database is invalid";
}
return false;
}
bool Database::mark(const QString &filename)
{
m_markQuery.bindValue(0, filename);
m_markQuery.exec();
return checkError(m_markQuery);
}
bool Database::unmark(const QString &filename)
{
m_unmarkQuery.bindValue(0, filename);
m_unmarkQuery.exec();
return checkError(m_unmarkQuery);
}
bool Database::mark(const QStringList &filenames)
{
m_markQuery.bindValue(0, filenames);
m_markQuery.execBatch();
return checkError(m_markQuery);
}
bool Database::unmark(const QStringList &filenames)
{
m_unmarkQuery.bindValue(0, filenames);
m_unmarkQuery.execBatch();
return checkError(m_unmarkQuery);
}
bool Database::isMarked(const QString &filename)
{
m_isMarkedQuery.bindValue(":name", filename);
m_isMarkedQuery.exec();
checkError(m_isMarkedQuery);
return m_isMarkedQuery.next();
}
QStringList Database::getMarkedFiles()
{
QSqlQuery markedFiles("SELECT * from files");
QStringList files;
while(markedFiles.next())
{
files << markedFiles.value("file").toString();
}
return files;
}
void Database::clearMarkedFiles()
{
QSqlQuery query("DELETE FROM files");
}
bool Database::checkError(QSqlQuery &query)
{
QSqlError error = query.lastError();
if(error.type() == QSqlError::NoError)
return true;
else
{
qDebug() << error.text();
return false;
}
}
int Database::checkVersion(QSqlDatabase &db)
{
QSqlQuery query("PRAGMA user_version", db);
if(query.next())
return query.value(0).toInt();
return -1;
}
static QStringList nameFilters = {"*.fit", "*.fits", "*.fz", "*.fts", "*.xisf"};
static int countFiles(const QDir &dir, QStringList &scannedDirs)
{
if(scannedDirs.contains(dir.canonicalPath()))return 0;
scannedDirs.append(dir.canonicalPath());
int count = dir.entryList(nameFilters, QDir::Files).size();
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for(const QString &d : dirs)
count += countFiles(dir.filePath(d), scannedDirs);
return count;
}
void Database::indexDir(const QDir &dir, QProgressDialog *progress)
{
m_progress = 0;
QStringList scannedDirs;
int count = countFiles(dir, scannedDirs);
progress->setMaximum(count);
database.transaction();
scannedDirs.clear();
if(indexDir2(dir, progress, scannedDirs))
{
database.commit();
emit databaseChanged();
}
else
{
database.rollback();
}
}
void Database::reindex(QProgressDialog *progress)
{
QVariantList deleteids;
database.transaction();
QSqlQuery size("SELECT COUNT(*) FROM fits_files", database);
size.next();
progress->setMaximum(size.value(0).toInt());
QSqlQuery files("SELECT id,file,mtime FROM fits_files", database);
int i = 0;
while(files.next())
{
QString path = files.value(1).toString();
QFileInfo file(path);
if(file.exists() && file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate) != files.value(2).toString())
indexFile(file);
if(!file.exists())
deleteids.append(files.value(0));
progress->setValue(i++);
if(progress->wasCanceled())
{
database.rollback();
return;
}
}
QSqlQuery deleteFiles("DELETE FROM fits_files WHERE id = ?", database);
deleteFiles.bindValue(0, deleteids);
deleteFiles.execBatch();
database.commit();
}
QStringList Database::getFitsKeywords()
{
m_headerKeywords.exec();
QStringList keywords;
while(m_headerKeywords.next())
{
keywords << m_headerKeywords.value(0).toString();
}
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;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
{
if(scannedDirs.contains(dir.canonicalPath()))return true;
scannedDirs.append(dir.canonicalPath());
QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for(const QString &d : dirs)
{
if(!indexDir2(dir.filePath(d), progress, scannedDirs))
return false;
}
for(const QFileInfo &file : files)
{
progress->setValue(m_progress++);
if(progress->wasCanceled())return false;
if(!indexFile(file))return false;
}
return true;
}
bool Database::indexFile(const QFileInfo &file)
{
ImageInfoData info;
QString filePath = file.absoluteFilePath();
QString mtime = file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate);
m_checkFile.bindValue(0, filePath);
m_checkFile.exec();
if(m_checkFile.next())
{
if(m_checkFile.value(1).toString() == mtime)
return true;
else
{
m_deleteFile.bindValue(0, m_checkFile.value(0).toLongLong());
m_deleteFile.exec();
}
}
bool ok = false;
if(isXISF(file.suffix()))
ok = readXISFHeader(filePath, info);
else if(isFITS(file.suffix()))
ok = readFITSHeader(filePath, info);
qlonglong last_id = -1;
if(ok)
{
if(info.wcs)
{
double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
info.wcs->calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
qDebug() << "bounds" << minRa << maxRa << minDec << maxDec;
m_insertFileWcs.bindValue(0, filePath);
m_insertFileWcs.bindValue(1, mtime);
m_insertFileWcs.bindValue(2, minRa);
m_insertFileWcs.bindValue(3, maxRa);
m_insertFileWcs.bindValue(4, minDec);
m_insertFileWcs.bindValue(5, maxDec);
m_insertFileWcs.bindValue(6, crVal1);
m_insertFileWcs.bindValue(7, crVal2);
if(!m_insertFileWcs.exec())
{
qDebug() << "Database error" << m_insertFileWcs.lastError();
return false;
}
last_id = m_insertFileWcs.lastInsertId().toLongLong();
}
else
{
m_insertFile.bindValue(0, filePath);
m_insertFile.bindValue(1, mtime);
if(!m_insertFile.exec())
{
qDebug() << "Database error" << m_insertFile.lastError();
return false;
}
last_id = m_insertFile.lastInsertId().toLongLong();
}
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();
comments << QString(record.comment);
}
m_insertFitsHeader.bindValue(0, file_id);
m_insertFitsHeader.bindValue(1, keys);
m_insertFitsHeader.bindValue(2, values);
m_insertFitsHeader.bindValue(3, comments);
if(!m_insertFitsHeader.execBatch())
{
qDebug() << "Database error" << m_insertFitsHeader.lastError();
return false;
}
}
qDebug() << "Indexed" << filePath << last_id;
return true;
}
+18 -4
View File
@@ -6,35 +6,49 @@
#include <QSqlQuery>
#include <QDir>
#include <QProgressDialog>
#include "imageinfodata.h"
class Database : public QObject
{
Q_OBJECT
QSqlDatabase m_database;
QSqlDatabase database;
QSqlDatabase ngc;
QSqlQuery m_markQuery;
QSqlQuery m_unmarkQuery;
QSqlQuery m_isMarkedQuery;
QSqlQuery m_insertFile;
QSqlQuery m_insertFileWcs;
QSqlQuery m_insertFitsHeader;
QSqlQuery m_checkFile;
QSqlQuery m_headerKeywords;
QSqlQuery m_deleteFile;
QSqlQuery m_getNgc;
int m_progress;
public:
explicit Database(QObject *parent = 0);
bool init();
bool init(const QLatin1String &connectionName = QLatin1String(QSqlDatabase::defaultConnection));
bool mark(const QString &filename);
bool unmark(const QString &filename);
bool mark(const QStringList &filenames);
bool unmark(const QStringList &filenames);
bool isMarked(const QString &filename);
QStringList getMarkedFiles();
void clearMarkedFiles();
void indexDir(const QDir &dir, QProgressDialog *progress);
void reindex(QProgressDialog *progress);
QStringList getFitsKeywords();
QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec);
protected:
bool indexDir2(const QDir &dir, QProgressDialog *progress);
bool checkError();
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
bool indexFile(const QFileInfo &file);
bool checkError(QSqlQuery &query);
int checkVersion(QSqlDatabase &db);
signals:
void databaseChanged();
};
#endif // DATABASE_H
+422
View File
@@ -0,0 +1,422 @@
#include "databaseview.h"
#include <QVBoxLayout>
#include <QPushButton>
#include <QSettings>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QSqlError>
#include <QDebug>
#include <QMenu>
#include <QContextMenuEvent>
#include <QRegularExpression>
#include <iostream>
#include "batchprocessing.h"
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
double RA(const QString &ra)
{
QRegularExpression reg("(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
QRegularExpressionMatch match = reg.match(ra);
double h = match.captured(1).toDouble();
double m = match.captured(2).toDouble();
double s = match.captured(3).toDouble();
qDebug() << "RA" << match.capturedTexts() << h << m << s;
return h*15 + m*0.25 + s*15/3600;
}
double DEC(const QString &dec)
{
QRegularExpression reg("([\\+\\-])?(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
QRegularExpressionMatch match = reg.match(dec);
double sign = match.captured(1) == "-" ? -1 : 1;
double d = match.captured(2).toDouble();
double m = match.captured(3).toDouble();
double s = match.captured(4).toDouble();
qDebug() << "DEC" << match.capturedTexts() << sign << d << m << s;
return sign * (d + m/60 + s/3600);
}
SelectColumnsDialog::SelectColumnsDialog(QWidget *parent) : QDialog(parent)
{
m_listWidget = new QListWidget(this);
m_listWidget->setSelectionMode(QAbstractItemView::MultiSelection);
QVBoxLayout *layout = new QVBoxLayout(this);
setLayout(layout);
layout->addWidget(m_listWidget);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttonBox);
setWindowTitle(tr("Select columns"));
}
void SelectColumnsDialog::setColumns(QStringList columns)
{
QSettings settings;
m_listWidget->addItems(columns);
QStringList selected = settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList();
for(auto &sel : selected)
{
int i = columns.indexOf(sel);
if(i>=0)
m_listWidget->item(i)->setSelected(true);
}
}
QStringList SelectColumnsDialog::selectedColumns()
{
QStringList ret;
for(auto &sel : m_listWidget->selectedItems())
ret.append(sel->text());
return ret;
}
FITSFileModel::FITSFileModel(Database *database, QObject *parent) : QSqlQueryModel(parent)
, m_database(database)
{
}
void FITSFileModel::sort(int column, Qt::SortOrder order)
{
if(column < 0)
m_sort.clear();
else if(column <= m_columns.size())
{
if(column == 0)m_sort = " ORDER BY f.file ";
else m_sort = QString(" ORDER BY \"h%1_value\" ").arg(column-1);
m_sort += order == Qt::AscendingOrder ? "ASC" : "DESC";
prepareQuery();
}
}
void FITSFileModel::setColumns(const QStringList &columns)
{
m_columns = columns;
prepareQuery();
}
void FITSFileModel::setFilter(const QStringList &key, const QStringList &value, const QStringList &limit)
{
if(value.isEmpty())
{
m_key.clear();
m_value.clear();
m_limit.clear();
}
else
{
m_key = key;
m_value = value;
m_limit = limit;
}
prepareQuery();
}
QVariant FITSFileModel::data(const QModelIndex &index, int role) const
{
if(role == Qt::FontRole && index.column() == 0)
{
QFont font;
QString file = index.data().toString();
font.setBold(m_markedFiles.contains(file));
return font;
}
if(role == Qt::ToolTipRole && index.column() == 0)
{
return QSqlQueryModel::data(index, Qt::DisplayRole);
}
return QSqlQueryModel::data(index, role);
}
void FITSFileModel::filesMarked(const QModelIndexList &indexes)
{
for(auto &index : indexes)
{
QString file = index.data().toString();
if(!m_markedFiles.contains(file))
{
m_markedFiles.insert(file);
emit dataChanged(index, index, {Qt::FontRole});
}
}
}
void FITSFileModel::filesUnmarked(const QModelIndexList &indexes)
{
for(auto &index : indexes)
{
QString file = index.data().toString();
if(m_markedFiles.contains(file))
{
m_markedFiles.remove(file);
emit dataChanged(index, index, {Qt::FontRole});
}
}
}
void FITSFileModel::prepareQuery()
{
QString cols;
QString join;
QStringList where;
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
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]));
else if(m_key[i] == "RA pos")
where.append(QString(" %1 BETWEEN f.minRa AND f.maxRa ").arg(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])));
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])));
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])));
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]);
}
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);
i++;
}
cols.chop(1);
sql += cols;
sql += " FROM fits_files AS f";
sql += join;
if(!where.isEmpty())sql += " WHERE " + where.join("AND");
sql += " GROUP BY f.id" + m_sort;
setQuery(sql);
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;
if(lastError().type() != QSqlError::NoError)
qDebug() << "Database error" << lastError();
QStringList list = m_database->getMarkedFiles();
m_markedFiles = QSet<QString>(list.begin(), list.end());
}
DatabaseTableView::DatabaseTableView(QWidget *parent) : QTableView(parent)
{
}
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 *a = menu.exec(event->globalPos());
if(a == nullptr)
return;
QModelIndexList indexes = selectionModel()->selectedRows();
if(a == mark)
emit filesMarked(indexes);
else if(a == unmark)
emit filesUnmarked(indexes);
else if(a == open)
emit openFile(indexes);
else if(a == openDirAction)
emit openDir(indexes);
}
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
,m_database(database)
{
QVBoxLayout *layout = new QVBoxLayout(this);
setLayout(layout);
m_tableView = new DatabaseTableView(this);
m_tableView->verticalHeader()->setDefaultSectionSize(1);
m_tableView->setSortingEnabled(true);
m_tableView->horizontalHeader()->setSortIndicatorShown(true);
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
layout->addWidget(m_tableView);
connect(m_tableView, &QTableView::activated, this, &DataBaseView::itemActivated);
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->horizontalHeader()->restoreState(settings.value("databaseview/header").toByteArray());
QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout);
QPushButton *selectColumnsButton = new QPushButton(tr("Select columns"), this);
hlayout->addWidget(selectColumnsButton);
connect(selectColumnsButton, &QPushButton::pressed, this, &DataBaseView::selectColumns);
connect(m_tableView, &DatabaseTableView::filesMarked, [this](QModelIndexList indexes){
QStringList files;
for(auto &index : indexes)
files.append(index.data().toString());
m_database->mark(files);
m_model->filesMarked(indexes);
});
connect(m_tableView, &DatabaseTableView::filesUnmarked, [this](QModelIndexList indexes){
QStringList files;
for(auto &index : indexes)
files.append(index.data().toString());
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)
{
combobox->clear();
combobox->addItem("file");
combobox->addItem("RA pos");
combobox->addItem("DEC pos");
combobox->addItem("RA range");
combobox->addItem("DEC range");
combobox->addItems(fitsKeywords);
};
QStringList fitsKeywords = m_database->getFitsKeywords();
for(int i=0; i<3; i++)
{
m_filterKeyword[i] = new QComboBox(this);
m_filterKeyword[i]->setMaximumWidth(300);
addFilterItems(m_filterKeyword[i], fitsKeywords);
m_search[i] = new QLineEdit(this);
m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));
m_limit[i] = new QLineEdit(this);
m_limit[i]->hide();
connect(m_filterKeyword[i], static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this, i](int index){
if(index == 3 || index == 4)m_limit[i]->show();
else m_limit[i]->hide();
});
connect(m_search[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
connect(m_limit[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
hlayout->addWidget(m_filterKeyword[i]);
hlayout->addWidget(m_search[i]);
hlayout->addWidget(m_limit[i]);
}
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
connect(filterButton, &QPushButton::pressed, this, &DataBaseView::applyFilter);
hlayout->addWidget(filterButton);
connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
QStringList fitsKeywords = m_database->getFitsKeywords();
for(int i=0; i<3; i++)
addFilterItems(m_filterKeyword[i], fitsKeywords);
applyFilter();
});
}
DataBaseView::~DataBaseView()
{
QSettings settings;
settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
}
void DataBaseView::selectColumns()
{
SelectColumnsDialog dialog;
QStringList columns = m_database->getFitsKeywords();
dialog.setColumns(columns);
if(dialog.exec() == QDialog::Accepted)
{
QSettings settings;
QStringList columns = dialog.selectedColumns();
settings.setValue("databaseview/selectedColumns", columns);
m_model->setColumns(columns);
}
}
void DataBaseView::loadDatabase()
{
QSettings settings;
m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
}
void DataBaseView::itemActivated(const QModelIndex &index)
{
emit loadFile(m_model->data(index.siblingAtColumn(0)).toString());
}
void DataBaseView::applyFilter()
{
QStringList keys;
QStringList values;
QStringList limits;
for(int i=0; i<3; i++)
{
QString key = m_filterKeyword[i]->currentText();
if(!m_search[i]->text().isEmpty() && !keys.contains(key))
{
keys.append(key);
values.append(m_search[i]->text());
limits.append(m_limit[i]->text());
}
}
m_model->setFilter(keys, values, limits);
}
bool DataBaseView::exportCSV(const QString &path)
{
QFile csv(path);
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
return false;
QSqlQuery sql(m_model->query().lastQuery());
int colCount = m_model->columnCount();
QStringList header;
for(int i=0; i<colCount; i++)
header.append(m_model->headerData(i, Qt::Horizontal).toString());
csv.write(header.join(",").toUtf8());
csv.write("\n");
while(sql.next())
{
QStringList columns;
for(int i=0; i<colCount; i++)
{
QString val = sql.value(i).toString();
val.replace("\"", "\"\"");
if(val.contains('"') || val.contains(','))
{
val.prepend('"');
val.append('"');
}
columns.append(val);
}
csv.write(columns.join(",").toUtf8());
csv.write("\n");
}
return true;
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef DATABASEVIEW_H
#define DATABASEVIEW_H
#include <QDialog>
#include <QWidget>
#include <QSqlQueryModel>
#include <QTableView>
#include <QListWidget>
#include <QComboBox>
#include <QLineEdit>
#include "database.h"
class SelectColumnsDialog : public QDialog
{
Q_OBJECT
QListWidget *m_listWidget;
public:
explicit SelectColumnsDialog(QWidget *parent = nullptr);
void setColumns(QStringList columns);
QStringList selectedColumns();
};
class FITSFileModel : public QSqlQueryModel
{
Q_OBJECT
QStringList m_columns;
QString m_sort;
QStringList m_key;
QStringList m_value;
QStringList m_limit;
QSet<QString> m_markedFiles;
Database *m_database;
public:
explicit FITSFileModel(Database *database, QObject *parent = nullptr);
void sort(int column, Qt::SortOrder order) override;
void setColumns(const QStringList &columns);
void setFilter(const QStringList &key, const QStringList &value, const QStringList &limit);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void filesMarked(const QModelIndexList &indexes);
void filesUnmarked(const QModelIndexList &indexes);
protected:
void prepareQuery();
};
class DatabaseTableView : public QTableView
{
Q_OBJECT
public:
explicit DatabaseTableView(QWidget *parent = nullptr);
protected:
void contextMenuEvent(QContextMenuEvent *event) override;
signals:
void filesMarked(QModelIndexList indexes);
void filesUnmarked(QModelIndexList indexes);
void openFile(QModelIndexList indexes);
void openDir(QModelIndexList indexes);
};
class DataBaseView : public QWidget
{
Q_OBJECT
Database *m_database;
DatabaseTableView *m_tableView;
FITSFileModel *m_model;
QComboBox *m_filterKeyword[3];
QLineEdit *m_search[3];
QLineEdit *m_limit[3];
public:
explicit DataBaseView(Database *database, QWidget *parent = nullptr);
~DataBaseView() override;
public slots:
void selectColumns();
void loadDatabase();
void itemActivated(const QModelIndex &index);
void applyFilter();
bool exportCSV(const QString &path);
signals:
void loadFile(QString file);
};
#endif // DATABASEVIEW_H
+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>
+163
View File
@@ -0,0 +1,163 @@
#include "filesystemwidget.h"
#include <QSettings>
#include <QVBoxLayout>
#include <QContextMenuEvent>
#include <QMenu>
#include <QSettings>
#include <QHeaderView>
#include <QMimeDatabase>
FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) : QWidget(parent)
, m_model(model)
{
m_listView = new QListView(this);
m_listView->setModel(model);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_listView);
setLayout(layout);
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilesystemWidget::fileClicked);
}
void FilesystemWidget::contextMenuEvent(QContextMenuEvent *event)
{
QMenu menu;
menu.addAction(tr("Sort by filename"), [this](){ emit sortChanged(QDir::Name); });
menu.addAction(tr("Sort by time"), [this](){ emit sortChanged(QDir::Time); });
menu.addAction(tr("Sort by size"), [this](){ emit sortChanged(QDir::Size); });
menu.addAction(tr("Sort by type"), [this](){ emit sortChanged(QDir::Type); });
menu.addAction(tr("Reverse"), [this](){ emit reverseSort(); });
menu.exec(event->globalPos());
}
void FilesystemWidget::selectFile(int row)
{
QModelIndex index = m_model->index(row, 0);
m_listView->selectionModel()->select(index, QItemSelectionModel::SelectCurrent);
m_listView->scrollTo(index);
}
void FilesystemWidget::fileClicked(const QModelIndex &index, const QModelIndex &)
{
if(index.isValid())
emit fileSelected(index.row());
}
QVariant FileSystemModel::data(const QModelIndex &index, int role) const
{
if(role == Qt::ToolTipRole && index.column() == 0)role = Qt::DisplayRole;
return QFileSystemModel::data(index, role);
}
Filetree::Filetree(QWidget *parent) : QTreeView(parent)
{
QSettings settings;
m_rootDir = settings.value("filetree/rootDir", QDir::homePath()).toString();
m_fileSystemModel = new FileSystemModel(this);
m_fileSystemModel->setRootPath(m_rootDir);
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);
setModel(m_fileSystemModel);
setRootIndex(m_fileSystemModel->index(m_rootDir));
header()->restoreState(settings.value("filetree/header").toByteArray());
}
Filetree::~Filetree()
{
QSettings settings;
settings.setValue("filetree/rootDir", m_rootDir);
settings.setValue("filetree/header", header()->saveState());
settings.setValue("filetree/showHidden", (bool)(m_fileSystemModel->filter() & QDir::Hidden));
}
void Filetree::contextMenuEvent(QContextMenuEvent *event)
{
QModelIndex index = indexAt(event->pos());
QFileInfo info = m_fileSystemModel->fileInfo(index);
QMenu menu;
QAction *open = nullptr;
QAction *setRoot = nullptr;
QAction *copy = nullptr;
QAction *move = nullptr;
QAction *indexDir = nullptr;
if(info.isFile())
open = menu.addAction(tr("Open"));
if(info.isDir())
{
open = menu.addAction(tr("Open"));
setRoot = menu.addAction(tr("Set as root"));
copy = menu.addAction(tr("Copy marked files"));
move = menu.addAction(tr("Move marked files"));
indexDir = menu.addAction(tr("Index directory"));
}
menu.addSeparator();
QAction *resetRoot = menu.addAction(tr("Reset root"));
QAction *goUp = menu.addAction(tr("Go up"));
QAction *showHidden = menu.addAction(tr("Show hidden files"));
showHidden->setCheckable(true);
showHidden->setChecked(m_fileSystemModel->filter() & QDir::Hidden);
QAction *a = menu.exec(event->globalPos());
if(a == nullptr)
return;
if(a == open)
{
emit fileSelected(m_fileSystemModel->filePath(index));
}
else if(a == setRoot && index.isValid())
{
setRootIndex(index);
m_rootDir = m_fileSystemModel->filePath(index);
m_fileSystemModel->setRootPath(m_rootDir);
}
else if(a == resetRoot)
{
setRootIndex(QModelIndex());
m_rootDir = QDir::rootPath();
}
else if(a == goUp)
{
setRootIndex(rootIndex().parent());
m_rootDir = m_fileSystemModel->filePath(rootIndex().parent());
m_fileSystemModel->setRootPath(m_rootDir);
}
else if(a == copy)
{
emit copyFiles(m_fileSystemModel->filePath(index));
}
else if(a == move)
{
emit moveFiles(m_fileSystemModel->filePath(index));
}
else if(a == indexDir)
{
emit indexDirectory(m_fileSystemModel->filePath(index));
}
else if(a == showHidden)
{
auto filter = m_fileSystemModel->filter();
filter ^= QDir::Hidden;
m_fileSystemModel->setFilter(filter);
m_fileSystemModel->setRootPath(m_rootDir);
}
}
void Filetree::mouseDoubleClickEvent(QMouseEvent *event)
{
QModelIndex index = indexAt(event->pos());
QFileInfo info = m_fileSystemModel->fileInfo(index);
if(info.isFile())
emit fileSelected(info.filePath());
else
QTreeView::mouseDoubleClickEvent(event);
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef FILESYSTEMWIDGET_H
#define FILESYSTEMWIDGET_H
#include <QWidget>
#include <QFileSystemModel>
#include <QIdentityProxyModel>
#include <QListView>
#include <QTreeView>
class FilesystemWidget : public QWidget
{
Q_OBJECT
QListView *m_listView;
QAbstractItemModel *m_model;
public:
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
void contextMenuEvent(QContextMenuEvent *event) override;
public slots:
void selectFile(int row);
protected slots:
void fileClicked(const QModelIndex &index, const QModelIndex &);
signals:
void fileSelected(int row);
void sortChanged(QDir::SortFlag sort);
void reverseSort();
};
class FileSystemModel : public QFileSystemModel
{
public:
explicit FileSystemModel(QObject *parent) : QFileSystemModel(parent){}
QVariant data(const QModelIndex &index, int role) const override;
};
class Filetree : public QTreeView
{
Q_OBJECT
FileSystemModel *m_fileSystemModel;
QString m_rootDir;
public:
explicit Filetree(QWidget *parent = nullptr);
~Filetree() override;
void contextMenuEvent(QContextMenuEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
signals:
void fileSelected(const QString &path);
void copyFiles(const QString &path);
void moveFiles(const QString &path);
void indexDirectory(const QString &path);
};
#endif // FILESYSTEMWIDGET_H
+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>
+85
View File
@@ -0,0 +1,85 @@
#include "histogram.h"
#include <cmath>
#include <algorithm>
#include <QPainter>
#include <QDebug>
#include <QStyleOption>
Histogram::Histogram(QWidget *parent) : QWidget(parent)
{
setStyleSheet("QWidget { background: white; color: black; } ");
}
void Histogram::imageLoaded(Image *img)
{
if(img && img->rawImage())
{
m_histogram = img->rawImage()->imageStats().m_histogram;
m_points.clear();
update();
}
}
void Histogram::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), Qt::black);
uint h = height();
uint w = width();
if(m_histogram.size())
{
if(m_points.size() != w)
{
m_points.clear();
for(uint64_t i = 0; i < w; i++)
{
uint32_t sum = 0;
uint64_t start = i * m_histogram.size() / w;
uint64_t end =(i+1) * m_histogram.size() / w;
for(uint64_t o = start; o < end; o++)
sum += m_histogram[o];
if(start != end)
m_points.push_back(sum);
}
float scale = *std::max_element(m_points.begin(), m_points.end());
if(m_log)
{
scale = std::log(scale);
std::for_each(m_points.begin(), m_points.end(), [scale](float &x){ x = (x > 0 ? std::log(x) : 0.0f) / scale; });
}
else
{
std::for_each(m_points.begin(), m_points.end(), [scale](float &x){ x /= scale; });
}
}
std::vector<QPointF> points;
points.push_back(QPointF(0, h));
for(size_t i = 0; i < m_points.size(); i++)
{
points.push_back(QPointF((float)i * w / m_points.size(), h - m_points[i] * h));
}
points.push_back(QPoint(w, h));
painter.setBrush(Qt::gray);
painter.setPen(Qt::white);
painter.drawPolygon(&points[0], points.size());
}
QStyleOptionButton button;
button.initFrom(this);
button.state = m_log ? QStyle::State_On : QStyle::State_Off;
button.text = tr("Logarithmic scale");
button.rect = style()->subElementRect(QStyle::SE_CheckBoxClickRect, &button, this);
button.rect.moveTop(0);
button.rect.moveRight(w);
style()->drawControl(QStyle::CE_CheckBox, &button, &painter, this);
}
void Histogram::mouseReleaseEvent(QMouseEvent *)
{
m_log = !m_log;
m_points.clear();
update();
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef HISTOGRAM_H
#define HISTOGRAM_H
#include <QWidget>
#include "imageringlist.h"
class Histogram : public QWidget
{
Q_OBJECT
std::vector<uint32_t> m_histogram;
std::vector<float> m_points;
bool m_log = false;
public:
explicit Histogram(QWidget *parent = nullptr);
public slots:
void imageLoaded(Image *img);
protected:
void paintEvent(QPaintEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
};
#endif // HISTOGRAM_H
+527
View File
@@ -0,0 +1,527 @@
#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);
_fw.open(QIODevice::WriteOnly | QIODevice::Truncate);
if(_fw.isOpen())
{
qDebug() << "open file" << _fw.fileName();
}
_dstream = ZSTD_createDStream();
}
Download::~Download()
{
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())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;
ZSTD_DStream *_dstream;
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
+9 -7
View File
@@ -2,12 +2,7 @@
#include <QSettings>
#include <QHeaderView>
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
bool FITSRecord::editable() const
{
return noEditableKey.count(key);
}
QMap<QString, QColor> headerHighlight;
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
{
@@ -32,7 +27,14 @@ void ImageInfo::setInfo(const ImageInfoData &info)
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
for(const FITSRecord &record : info.fitsHeader)
{
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString(), record.comment});
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);
}
+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
+574
View File
@@ -0,0 +1,574 @@
#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);
}
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
+607
View File
@@ -0,0 +1,607 @@
#include "imageringlist.h"
#include <functional>
#include <QThreadPool>
#include <QDir>
#include <QSettings>
#include <QTimer>
#include <QRegularExpression>
#include "loadrunable.h"
#include "rawimage.h"
#include "database.h"
using namespace std;
int DEFAULT_WIDTH = 2;
Image::Image(const QString name, int number, ImageRingList *ringList) :
m_loading(false),
m_released(true),
m_current(false),
m_number(number),
m_name(name),
m_ringList(ringList)
{
}
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;
pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel(), index));
}
if(!m_loading && m_rawImage)
emit pixmapLoaded(this);
}
void Image::loadThumbnail(QThreadPool *pool)
{
if(!m_thumbnail)
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, 0, true));
else
emit thumbnailLoaded(this);
}
void Image::release()
{
m_rawImage.reset();
m_released = true;
m_loading = false;
}
QString Image::name() const
{
return m_name;
}
std::shared_ptr<RawImage> Image::rawImage()
{
return m_rawImage;
}
const RawImage *Image::thumbnail() const
{
return m_thumbnail.get();
}
ImageInfoData Image::info() const
{
return m_info;
}
bool Image::isCurrent() const
{
return !m_released;
}
int Image::number() const
{
return m_number;
}
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;
if(!m_released)
{
m_rawImage = rawImage;
m_info = info;
emit pixmapLoaded(this);
}
}
void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
{
m_thumbnail = rawImage;
if(m_thumbnail)
emit thumbnailLoaded(this);
}
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractItemModel(parent)
, m_liveMode(false)
, m_analyzeLevel(None)
, m_database(database)
, m_nameFilter(nameFilter)
, m_fileSuffix(nameFilter)
{
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()
{
m_loadPool->clear();
m_thumbPool->clear();
m_loadPool->waitForDone();
m_thumbPool->waitForDone();
}
bool ImageRingList::setDir(const QString path, const QString &currentFile, bool recursive)
{
QDir dir(path);
if(dir.exists())
{
m_currentDir = path;
QStringList scannedDirs;
QStringList absolutePaths;
std::function<void(const QString&)> scanDir = [&](const QString &path)
{
QDir dir(path);
if(scannedDirs.contains(dir.canonicalPath()))return;
scannedDirs.append(dir.canonicalPath());
QDir::SortFlags sortFlags = m_liveMode ? QDir::Time : m_sort | QDir::IgnoreCase;
if(m_reversed)sortFlags |= QDir::Reversed;
if(recursive)
{
QStringList dirs = dir.entryList(QDir::Readable | QDir::Dirs | QDir::NoDotAndDotDot, sortFlags);
for(const QString &subdir : dirs)
scanDir(dir.absoluteFilePath(subdir));
}
QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, sortFlags);
for(const QString &file : list)
{
absolutePaths.append(dir.absoluteFilePath(file));
}
};
scanDir(path);
//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;
}
return false;
}
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()
{
if(m_images.size())
return *m_currImage;
else
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)->isLoading())
return;
(*m_firstImage)->release();
m_firstImage = increment(m_firstImage);
m_currImage = increment(m_currImage);
(*m_currImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load(0, m_loadPool);
}
}
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(0, m_loadPool);
m_lastImage = decrement(m_lastImage);
(*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);
}
}
void ImageRingList::setLiveMode(bool live)
{
m_liveMode = live;
}
void ImageRingList::setCalculateStats(bool stats)
{
m_analyzeLevel = stats ? Statistics : None;
}
void ImageRingList::setFindPeaks(bool findPeaks)
{
m_analyzeLevel = findPeaks ? Peaks : None;
}
void ImageRingList::setFindStars(bool findStars)
{
m_analyzeLevel = findStars ? Stars : None;
}
AnalyzeLevel ImageRingList::analyzeLevel() const
{
return m_analyzeLevel;
}
void ImageRingList::loadFile(int row)
{
if(row < m_images.size())
{
int diff = m_currImage != m_images.end() ? row - (m_currImage - m_images.begin()) : -100;
if(diff == 1)
increment();
else if(diff == -1)
decrement();
else
{
m_firstImage = m_currImage = m_lastImage = m_images.begin()+row;
if(m_images.empty())
return;
(*m_currImage)->load(0, m_loadPool);
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
if(m_liveMode)
m_width = 0;
for(int i=0; i<m_width; i++)
{
m_firstImage = decrement(m_firstImage);
(*m_firstImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load(0, m_loadPool);
}
if(m_lastImage != m_firstImage)
{
QList<ImagePtr>::iterator iter = increment(m_lastImage);
while(m_firstImage != iter)
{
(*iter)->release();
iter = increment(iter);
}
}
}
}
}
void ImageRingList::loadThumbnails()
{
for(auto &img : m_images)
{
img->loadThumbnail(m_thumbPool);
}
}
void ImageRingList::stopLoading()
{
m_thumbPool->clear();
m_thumbPool->waitForDone();
}
int ImageRingList::imageCount() const
{
return m_images.size();
}
QStringList ImageRingList::imageNames() const
{
QStringList ret;
for(auto &img : m_images)
ret.push_back(img->name());
return ret;
}
void ImageRingList::updateMark()
{
if(m_images.size())
{
QModelIndex idx = index(m_currImage - m_images.begin(), 0);
emit dataChanged(idx, idx, {Qt::FontRole});
}
}
void ImageRingList::clearThumbnails()
{
for(auto &img : m_images)
img->clearThumbnail();
}
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
{
Q_UNUSED(parent);
return createIndex(row, column, m_images.at(row).get());
}
QModelIndex ImageRingList::parent(const QModelIndex &child) const
{
Q_UNUSED(child);
return QModelIndex();
}
int ImageRingList::rowCount(const QModelIndex &parent) const
{
if(parent == QModelIndex())
return m_images.size();
else
return 0;
}
int ImageRingList::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QVariant ImageRingList::data(const QModelIndex &index, int role) const
{
switch(role)
{
case Qt::DisplayRole:
{
QFileInfo info(m_images.at(index.row())->name());
return info.fileName();
}
case Qt::FontRole:
{
bool marked = m_database->isMarked(m_images.at(index.row())->name());
QFont font;
font.setBold(marked);
return font;
}
default:
return QVariant();
}
}
QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int role) const
{
if(section==0 && orientation==Qt::Horizontal && role==Qt::DisplayRole)
{
return tr("Name");
}
return QVariant();
}
void ImageRingList::setPreload(int width)
{
DEFAULT_WIDTH = width;
if(m_images.size() == 0)return;
int newWidth = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
if(newWidth > m_width)
{
for(int i = newWidth - m_width; i>0; i--)
{
m_firstImage = decrement(m_firstImage);
(*m_firstImage)->load(0, m_loadPool);
m_lastImage = increment(m_lastImage);
(*m_lastImage)->load(0, m_loadPool);
}
}
if(newWidth < m_width)
{
for(int i = m_width - newWidth; i>0; i--)
{
(*m_firstImage)->release();
m_firstImage = increment(m_firstImage);
(*m_lastImage)->release();
m_lastImage = decrement(m_lastImage);
}
}
m_width = newWidth;
}
void ImageRingList::setSort(QDir::SortFlag sort)
{
if(m_sort != sort)
{
m_sort = sort;
if(m_images.size())
{
QString path = (*m_currImage)->name();
setFile(path);
}
}
}
void ImageRingList::reverseSort()
{
m_reversed = !m_reversed;
if(m_images.size())
{
QString path = (*m_currImage)->name();
setFile(path);
}
}
void ImageRingList::toggleSlideshow(bool start)
{
if(start)
{
QSettings settings;
int time = settings.value("settings/slideshowtime", 1.0).toDouble() * 1000;
m_slideShowTimer->start(time);
}
else
{
m_slideShowTimer->stop();
}
}
void ImageRingList::setFilesPrivate(const QStringList files, const QString &currentFile)
{
m_loadPool->clear();
m_thumbPool->clear();
m_loadPool->waitForDone();
m_thumbPool->waitForDone();
beginResetModel();
m_images.clear();
int i = 0;
for(const QString &file : files)
{
ImagePtr ptr = make_shared<Image>(file, i++, this);
connect(ptr.get(), &Image::pixmapLoaded, this, &ImageRingList::imageLoaded);
connect(ptr.get(), &Image::thumbnailLoaded, this, &ImageRingList::thumbnailLoaded);
m_images.append(ptr);
}
int index = files.indexOf(currentFile);
if(index < 0)
index = 0;
endResetModel();
m_currImage = m_images.end();
loadFile(index);
}
QList<ImagePtr>::iterator ImageRingList::increment(QList<ImagePtr>::iterator iter)
{
iter++;
if(iter == m_images.end())
iter = m_images.begin();
return iter;
}
QList<ImagePtr>::iterator ImageRingList::decrement(QList<ImagePtr>::iterator iter)
{
if(iter == m_images.begin())
iter = m_images.end();
iter--;
return iter;
}
void ImageRingList::imageLoaded(Image *image)
{
if(image->name() == (*m_currImage)->name())
{
emit pixmapLoaded(image);
emit infoLoaded(image->info());
emit currentImageChanged(m_currImage-m_images.begin());
}
}
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(m_currentDir, currentFile);
if(m_images.size())
emit currentImageChanged(m_currImage-m_images.begin());
}
+56 -13
View File
@@ -5,11 +5,14 @@
#include <QFileSystemWatcher>
#include <QList>
#include <QPixmap>
#include <QDir>
#include <memory>
#include "imageinfo.h"
#include "imageinfodata.h"
#include "rawimage.h"
#include <QAbstractItemModel>
class ImageRingList;
class QThreadPool;
class Image : public QObject
{
@@ -17,26 +20,37 @@ class Image : public QObject
bool m_loading;
bool m_released;
bool m_current;
std::unique_ptr<RawImage> m_rawImage;
int m_number;
std::shared_ptr<RawImage> m_rawImage;
std::shared_ptr<RawImage> m_thumbnail;
QString m_name;
ImageInfoData m_info;
ImageRingList *m_ringList;
public:
explicit Image(const QString name, ImageRingList *ringList);
void load();
explicit Image(const QString name, int number, ImageRingList *ringList);
void load(int index, QThreadPool *pool);
void loadThumbnail(QThreadPool *pool);
void release();
QString name() const;
RawImage* rawImage();
std::shared_ptr<RawImage> rawImage();
const RawImage* thumbnail() const;
ImageInfoData info() const;
bool isCurrent() const;
int number() const;
void clearThumbnail();
bool isLoading() const;
signals:
void pixmapLoaded(Image *ptr);
void thumbnailLoaded(Image *ptr);
protected slots:
void imageLoaded(void *rawImage, ImageInfoData info);
void imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info);
void thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage);
};
typedef std::shared_ptr<Image> ImagePtr;
class Database;
class ImageRingList : public QAbstractItemModel
{
Q_OBJECT
@@ -47,21 +61,37 @@ class ImageRingList : public QAbstractItemModel
QList<ImagePtr>::iterator m_lastImage;
QFileSystemWatcher m_fileSystemWatcher;
bool m_liveMode;
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(QObject *parent = 0);
~ImageRingList();
bool setDir(const QString path, const QString &currentFile = QString());
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();
void increment();
void decrement();
QString currentDir() const;
void setLiveMode(bool live);
void setCalculateStats(bool stats);
void setFindPeaks(bool findPeaks);
void setFindStars(bool findStars);
AnalyzeLevel analyzeLevel() const;
void loadFile(int row);
void loadThumbnails();
void stopLoading();
int imageCount() const;
QStringList imageNames() const;
void updateMark();
void clearThumbnails();
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
@@ -69,17 +99,30 @@ public:
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:
void setPreload(int width);
void setSort(QDir::SortFlag sort);
void reverseSort();
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:
void pixmapLoaded(Image *image);
void thumbnailLoaded(Image *image);
void infoLoaded(ImageInfoData info);
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
+1042
View File
File diff suppressed because it is too large Load Diff

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