Compare commits
268 Commits
20230212
...
6eda2c4e48
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eda2c4e48 | |||
| b16ae3a9ee | |||
| 56bba27ae3 | |||
| 1070dc32c1 | |||
| f61cf12f0a | |||
| 530b0c62c3 | |||
| 7e95440dd6 | |||
| 03492972cb | |||
| 9cca183677 | |||
| afd059b36b | |||
| 1b9f218acb | |||
| 32f91d7b2f | |||
| 69fbad34b6 | |||
| e026042604 | |||
| bb7e5182af | |||
| f0152e2496 | |||
| eccf928032 | |||
| 897306d1c3 | |||
| cbc779090f | |||
| d826744f26 | |||
| 3bdfb12d4f | |||
| c416ae9941 | |||
| abbba2890f | |||
| 9c6847d334 | |||
| af1c26a9fe | |||
| e0441a6494 | |||
| a88f05a9fe | |||
| b58559a18a | |||
| 2ac14a6c04 | |||
| b84256625c | |||
| 202a2b11b7 | |||
| 32f192ed7e | |||
| a0422683bd | |||
| ce67b35bfa | |||
| f016500f12 | |||
| 6069ebbbac | |||
| e587d84e05 | |||
| c01f2e328a | |||
| 8b498bbe73 | |||
| c6bc792ff7 | |||
| 1a307d82f9 | |||
| 8c5e2b2ebf | |||
| 03ad135ef0 | |||
| 2a78a9a41d | |||
| 1a214a169e | |||
| f8704c51d8 | |||
| 3feee0256c | |||
| 53472d807c | |||
| 9f06269aa4 | |||
| 78f242d808 | |||
| e6bab45a89 | |||
| 58286d52c5 | |||
| bac1963fa4 | |||
| 2415717ce0 | |||
| e7acbca01e | |||
| 7c4118b0b6 | |||
| 8178efdafd | |||
| 90026f931f | |||
| eee4613b25 | |||
| 24a9e96bbf | |||
| 5af5f4f068 | |||
| 85f9822b96 | |||
| 7fc6c64fd7 | |||
| 4488c2e6af | |||
| 0047607c1d | |||
| 45c368bbbb | |||
| c96cb86a29 | |||
| fe3e5f66be | |||
| 6fd17fbdf5 | |||
| f30dd2a520 | |||
| 21675d9479 | |||
| f669baa8a6 | |||
| c317012c99 | |||
| d0dbef20c7 | |||
| bd45900821 | |||
| 96a89bff92 | |||
| c05fc36ee3 | |||
| 05b0aa9a2f | |||
| 7b70b6cce5 | |||
| 5150ec5639 | |||
| 79529552d9 | |||
| c872c72bb5 | |||
| 58abf762c0 | |||
| e47c99fd21 | |||
| 24ddf1dc61 | |||
| 8f333191c3 | |||
| e4cb99657e | |||
| d644e8095d | |||
| 1796e128ad | |||
| 37fdac39dc | |||
| 13e1abf07e | |||
| 617abf7afe | |||
| d59ee7fddc | |||
| d545c6ca0f | |||
| 5249b277ec | |||
| e4b9fefa5a | |||
| d069ce3302 | |||
| 58c182adc0 | |||
| c36068aaf4 | |||
| fcb3aec81f | |||
| 7510dac82b | |||
| 55439be04c | |||
| 0ff2001797 | |||
| fc36024eee | |||
| 3cda53f26c | |||
| 58d18cc28a | |||
| 2b96da60de | |||
| 236f66ed2f | |||
| a86c100e69 | |||
| 45ee9b7258 | |||
| be1e65251d | |||
| 9b7837e9fb | |||
| 4afa940886 | |||
| d1344d2dc8 | |||
| 24eea573e6 | |||
| 8f7f527732 | |||
| 3635ac00cb | |||
| eba9110933 | |||
| 464207beb1 | |||
| 4aeff61c44 | |||
| 790c836bbd | |||
| 62616898ed | |||
| e216af6a6d | |||
| e0d6f417a0 | |||
| 3c5fef988e | |||
| 6c42315f87 | |||
| 06b3dbc1bb | |||
| 258553a6bb | |||
| 9c40ce2daa | |||
| 9f4c4c8bdc | |||
| da1aa4c6fc | |||
| a43f12565d | |||
| 32973c54ce | |||
| dccb2e88da | |||
| c8898387fe | |||
| dfe31b6350 | |||
| 553e72a5ce | |||
| 12901c9a47 | |||
| da79197376 | |||
| 30960033c5 | |||
| efd3ff35f3 | |||
| 52bcb10da1 | |||
| 9adfbde512 | |||
| e38de510a0 | |||
| 5e18c591f7 | |||
| d6e257e201 | |||
| 2c7a7d473f | |||
| 87d7bd2d9f | |||
| 79dd7d91eb | |||
| 21b4e0934c | |||
| 0239aba165 | |||
| 1b08242433 | |||
| a56e8a2c27 | |||
| bf360c1ae1 | |||
| 1ec3a6cffd | |||
| 100f47746c | |||
| 02bac0c850 | |||
| bc29dc7d34 | |||
| ff5053b626 | |||
| 511802bdbd | |||
| dd16a02045 | |||
| 7ed38cf6d7 | |||
| 8c6b451564 | |||
| d288810d5d | |||
| fb66e82428 | |||
| 71486efeef | |||
| 8213f6213f | |||
| f8c9fec77e | |||
| af4be850cb | |||
| ca1a13ed9d | |||
| 1873da6c49 | |||
| 92345f82ca | |||
| 3c8f49e932 | |||
| 37dd97e361 | |||
| da31187aa3 | |||
| 4801338160 | |||
| fb9d026ff5 | |||
| c2810faf8f | |||
| d3d302fd38 | |||
| a0497c7d19 | |||
| 8818e25eda | |||
| c3baa18087 | |||
| 66f0c05a48 | |||
| 461ffea874 | |||
| 7535ad87e7 | |||
| 273aef1594 | |||
| 9519c9830c | |||
| 342e5cc5db | |||
| ae84cbdfe0 | |||
| 933fd4a2a0 | |||
| c3588e1c36 | |||
| 174134a9ee | |||
| bbc13ec8a5 | |||
| 9f7e2ab6b4 | |||
| 4fe56acbd9 | |||
| f35db9d1af | |||
| 81d138f799 | |||
| ae07d4793b | |||
| dc2a781d3b | |||
| 90035f44ed | |||
| 53c9a58125 | |||
| 3f7e3689e8 | |||
| af9187737f | |||
| 4e952873e3 | |||
| fb24800050 | |||
| ea0dcc226a | |||
| 6a7b677b95 | |||
| 0cee4c9c53 | |||
| d5f2351905 | |||
| 18732a8cbf | |||
| 8c9c1d8d06 | |||
| e5f425ff8d | |||
| 428f9c360a | |||
| a8a1509db7 | |||
| 6539c78c57 | |||
| 0e0d29320e | |||
| 1efe8e6974 | |||
| dae10182d1 | |||
| ed5fc9c1c2 | |||
| cd6a64a98b | |||
| 67355a82b7 | |||
| 8fc2078a3a | |||
| da9b389409 | |||
| 7818b8d3e9 | |||
| 11294bfcb0 | |||
| faecb385aa | |||
| e5be04926b | |||
| eaf2c7094b | |||
| aef41f5f6b | |||
| 2134f13b06 | |||
| 0e9c980325 | |||
| b9bf6bf183 | |||
| 50c070b169 | |||
| cfee287bfa | |||
| 61e0c542f5 | |||
| a42abb05ea | |||
| 5c6df4a59f | |||
| 35d5934227 | |||
| 8e3c1b35db | |||
| 544e4abf92 | |||
| e97e10fb5b | |||
| 2608a1bc79 | |||
| fa69f17e51 | |||
| 4a9d720343 | |||
| d462ece7c9 | |||
| 46b0210078 | |||
| 0a803ace10 | |||
| 0c2c5f908c | |||
| c2197298a7 | |||
| e8630330b2 | |||
| 5955a02175 | |||
| c0b9194ecc | |||
| 5f27acbfd1 | |||
| f1a2aae9b6 | |||
| 9ffbdcee30 | |||
| d9b1c253db | |||
| 7e39304799 | |||
| 31cf1ee2b1 | |||
| ab245f0484 | |||
| 77c312800a | |||
| 21e90b92dc | |||
| 2817d3c7c9 | |||
| a51b0ef02c | |||
| 7b19230366 | |||
| 26666ee36d | |||
| 74aee15f80 | |||
| fde1594086 | |||
| a9783f6030 |
@@ -12,82 +12,116 @@ 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)
|
||||
find_library(GSLCBLAS_LIB gslcblas 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 REQUIRED)
|
||||
find_library(WCS_LIB wcs wcslib PATHS REQUIRED)
|
||||
find_library(WCS_LIB wcs wcslib REQUIRED)
|
||||
find_library(LCMS2_LIB lcms2 REQUIRED)
|
||||
find_library(STELLARSOLVER_LIB NAMES stellarsolver stellarsolver6)
|
||||
|
||||
add_subdirectory(libXISF)
|
||||
|
||||
set(TENMON_SRC
|
||||
about.cpp
|
||||
database.cpp
|
||||
databaseview.cpp
|
||||
delete.cpp
|
||||
filesystemwidget.cpp
|
||||
imageinfo.cpp
|
||||
imageringlist.cpp
|
||||
imagescrollarea.cpp
|
||||
imagescrollareagl.cpp
|
||||
loadrunable.cpp
|
||||
main.cpp
|
||||
mainwindow.cpp
|
||||
markedfiles.cpp
|
||||
rawimage.cpp
|
||||
settingsdialog.cpp
|
||||
starfit.cpp
|
||||
statusbar.cpp
|
||||
stfslider.cpp
|
||||
stretchtoolbar.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
|
||||
)
|
||||
|
||||
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
|
||||
if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
|
||||
set(COLOR_MANAGMENT OFF)
|
||||
endif(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
|
||||
|
||||
if(COLOR_MANAGMENT)
|
||||
add_compile_definitions("COLOR_MANAGMENT")
|
||||
endif(COLOR_MANAGMENT)
|
||||
|
||||
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)
|
||||
list(APPEND TENMON_SRC resources/icon.rc)
|
||||
set(tenmon_ICON "")
|
||||
elseif(APPLE)
|
||||
set(tenmon_ICON ${CMAKE_CURRENT_SOURCE_DIR}/tenmon.icns)
|
||||
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(PkgConfig REQUIRED)
|
||||
pkg_search_module(GIO REQUIRED gio-2.0)
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
endif()
|
||||
|
||||
add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
|
||||
qt_add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
|
||||
|
||||
find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED)
|
||||
target_include_directories(tenmon PRIVATE ${OpenCV_INCLUDE_DIRS} ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR})
|
||||
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR} "src")
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS})
|
||||
endif()
|
||||
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
|
||||
if(COLOR_MANAGMENT)
|
||||
target_compile_definitions(tenmon PRIVATE "COLOR_MANAGMENT")
|
||||
endif(COLOR_MANAGMENT)
|
||||
|
||||
target_link_libraries(tenmon Qt5::Widgets Qt5::Sql ${OpenCV_LIBS} ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} XISF)
|
||||
find_path(STELLARSOLVER_INCLUDE stellarsolver.h PATH_SUFFIXES libstellarsolver)
|
||||
if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
|
||||
target_include_directories(tenmon PRIVATE ${STELLARSOLVER_INCLUDE})
|
||||
if(MXE)
|
||||
find_library(GSL_LIB gsl REQUIRED)
|
||||
find_library(GSLCBLAS_LIB gslcblas REQUIRED)
|
||||
target_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 "-framework CoreFoundation")
|
||||
else()
|
||||
target_link_libraries(tenmon ${GIO_LDFLAGS})
|
||||
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 jasper)
|
||||
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)
|
||||
@@ -96,8 +130,9 @@ if(UNIX AND NOT APPLE)
|
||||
install(SCRIPT install.cmake)
|
||||
else()
|
||||
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
||||
install(FILES space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
|
||||
install(FILES space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
|
||||
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)
|
||||
@@ -111,3 +146,5 @@ else()
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -Dlocal_dir=${CMAKE_CURRENT_SOURCE_DIR} -Doutput_dir=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/gitversion.cmake")
|
||||
endif()
|
||||
|
||||
add_subdirectory(thumbnailer)
|
||||
|
||||
@@ -2,23 +2,32 @@ 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 libgsl-dev wcslib-dev libopencv-dev cmake
|
||||
sudo apt install qt6-base-dev qt6-declarative-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev wcslib-dev cmake libzstd-dev libqt6sql6-sqlite
|
||||
|
||||
on OpenSUSE
|
||||
|
||||
sudo zypper install opencv-devel gsl-devel exif-devel libraw-devel wcslib-devel libqt5-qtbase-devel
|
||||
sudo zypper install libexif-devel libraw-devel wcslib-devel qt6-base-devel qt6-qml-devel libzstd-devel
|
||||
|
||||
MacOS X
|
||||
|
||||
To compile on MacOS install XCode first. Then install homebrew in x86_64 mode
|
||||
with "arch -i x86_64". Building on native ARM is not supported.
|
||||
To compile on MacOS install XCode first. Then install homebrew.
|
||||
|
||||
homebrew install qt5 libraw cfitsio libexif libgsl wcslib opencv
|
||||
homebrew install qt6 libraw cfitsio libexif libgsl wcslib
|
||||
|
||||
You may need to set CMAKE_PREFIX_PATH for Qt5 and OpenCV so CMake can find them.
|
||||
You may need to set CMAKE_PREFIX_PATH for Qt6 so CMake can find them.
|
||||
|
||||
Then to build run standard cmake
|
||||
First run this command to get libXISF updated
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
Then to build run standard cmake sequence
|
||||
|
||||
cmake -B build -S .
|
||||
cmake --build build
|
||||
./build/tenmon
|
||||
|
||||
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/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Help</title>
|
||||
<style type="text/css">
|
||||
h1, h2, h3, h4 { padding:0px; margin:10px; }
|
||||
p { padding:0px; margin:5px; }
|
||||
@@ -9,14 +11,13 @@ img { margin: 5px; }
|
||||
<body>
|
||||
<h2>Tenmon help</h2>
|
||||
|
||||
<p>Tenmon is intended primarily for viewing astro photos and images. It supports the following formats:
|
||||
<p>Tenmon is intended primarily for viewing astro photos and images. It supports the following formats:</p>
|
||||
<ul>
|
||||
<li>FITS 8, 16 bit integer and 32 bit float</li>
|
||||
<li>XISF 8, 16 bit integer and 32 bit float</li>
|
||||
<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, NEF, DNG raw images</li>
|
||||
<li>CR2, CR3, NEF, DNG raw images</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Main windows</h3>
|
||||
<p>The main window shows the currently loaded image. On the left is the <i>Image info</i> panel which displays details about the loaded image.
|
||||
@@ -35,32 +36,41 @@ In the case of saving JPEG or PNG, the stretch function is applied to the saved
|
||||
To open an image, you can also drag and drop it to main window.</p>
|
||||
|
||||
<h3>View</h3>
|
||||
<p>The <i>View</i> menu has options to control the size and scale of displayed images:
|
||||
<p>The <i>View</i> menu has options to control the size and scale of displayed images:</p>
|
||||
<ul>
|
||||
<li><i>Zoom In</i> and <i>Zoom Out</i> magnify and shrink the image. The mouse wheel can be also used to zoom freely.</li>
|
||||
<li><i>Best fit</i> auto-zooms the image to fit the current size of the window.</li>
|
||||
<li><i>100%</i> will zoom to 1:1 scale.</li>
|
||||
<li><i>Bayer mask</i> set which bayer mask should be used when doing demosaicing.</li>
|
||||
<li><i>Colormap</i> select color pallette when showing image with false colours.</li>
|
||||
<li><i>Fullscreen</i> enlarges the main window to the whole screen.</li>
|
||||
<li><i>Thumbnails</i> will display small thumbnails for all images in the current directory.</li>
|
||||
<li><i>Slideshow</i> start showing all images periodically with interval that can be set in settings.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>Colormap can also be user defined. Place image file named colormap.png into application data directory.
|
||||
On Windows"C:/Users/<USER>/AppData/Roaming/nou/Tenmon" Linux: "~/.local/share/nou/Tenmon/" MacOS: "~/Library/Application Support/nou/Tenmon/"
|
||||
This image should be 256 pixel wide. Each row of image will be used as separate color map and added to Colormap menu.</p>
|
||||
|
||||
|
||||
<h3>Stretch toolbar</h3>
|
||||
<p>This panel changes how images are displayed.
|
||||
<br><img src=":/about/stretch-panel.png"></p>
|
||||
<p>Starting on the left, there is slider scale with three adjustable points to manually control the stretch.
|
||||
<br><img src=":/about/stretch-panel.png" alt="Stretch panel"></p>
|
||||
<p>Starting on the left, there is slider scale with three adjustable points to manually control the stretch.</p>
|
||||
<ul>
|
||||
<li>black point - all pixels with lower value (darker) than this setting will be clipped black</li>
|
||||
<li>mid point - defines the value to be stretched to 50% intensity</li>
|
||||
<li>white point - all pixels with higher value (brighter) than this will be clipped white</li>
|
||||
</ul>
|
||||
Following the slider are 5 buttons for automatic stretching:
|
||||
<p>Following the slider are 8 buttons to control image display:</p>
|
||||
<ul>
|
||||
<li><i>Linked channels</i> toggle stretching each RGB channel individually.</li>
|
||||
<li><i>Auto Stretch</i> automatically apply black and mid points to render the image with optimal brightness.</li>
|
||||
<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>Super pixel CFA </i> average 2x2 pixels into one (suitable for images from colour camera).</li>
|
||||
<li><i>Apply Auto stretch on load</i> toggle automatically applying Autostretch for each image when loaded.</p>
|
||||
<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>
|
||||
@@ -75,13 +85,12 @@ mouse button and drag across thumbnails to mark them. Holding <i>Ctrl</i> will u
|
||||
<h3>File system and tree</h3>
|
||||
<p>File system panel contain list of images in current opened directory. You can select file from this list and it will be displayed. It is also possible to
|
||||
use arrow keys to go back (left and up) and forth (right and down) between images.</p>
|
||||
<p>File tree show file system structure. You can right click to show context menu to perform various actions from <i>File</i> menu. There are also few others
|
||||
<p>File tree show file system structure. You can right click to show context menu to perform various actions from <i>File</i> menu. There are also few others</p>
|
||||
<ul>
|
||||
<li><i>Set as root directory</i> show only this directory and subdirectories</li>
|
||||
<li><i>Reset root directory</i> show whole file system</li>
|
||||
<li><i>Go up</i> show directory that is one level above current root directory</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Database of FITS/XISF files</h3>
|
||||
<p>Tenmon can scan a directory of FITS/XISF files and index metadata from FITS headers into it's internal database. This allows searching and sorting images based on that metadata.</p>
|
||||
@@ -103,16 +112,229 @@ Setting both "RA pos" and "DEC pos" can return images that doesn't contain enter
|
||||
"RA range" and "DEC range" filter out images which center coordinate is within entered range.
|
||||
Pressing Enter or clicking on <i>Filter</i> button will filter out database record according to search parameter.
|
||||
|
||||
<p>Wildcards:
|
||||
<p>Wildcards:</p>
|
||||
<ul>
|
||||
<li><b>%</b> (percent) is a wildcard representing zero or more of any characters.</li>
|
||||
<li><b>_</b> (underscore) is a wildcard for exactly one of any character.</i>
|
||||
<li><b>_</b> (underscore) is a wildcard for exactly one of any character.</li>
|
||||
<li>Without wildcard characters, the exact string must match.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br><img src=":/about/filter.png"><br>
|
||||
<p><img src=":/about/filter.png" alt="Filter"><br>
|
||||
This example filters for files where: "Bias" is in the file name, the OBJECT property is "M_42" (where the underscore can be any single character), and the DATE property begins with "2022".
|
||||
</p>
|
||||
|
||||
<h3>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/<USER>/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>
|
||||
|
||||
@@ -14,7 +14,7 @@ img { margin: 5px; }
|
||||
<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, NEF, DNG</li>
|
||||
<li>images RAW CR2, CR3, NEF, DNG</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
@@ -101,6 +101,172 @@ En appuyant sur la touche Enter ou en cliquant sur le bouton <i>Filtre</i>, les
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Pomocník</title>
|
||||
<style type="text/css">
|
||||
h1, h2, h3, h4 { padding:0px; margin:10px; }
|
||||
p { padding:0px; margin:5px 5px 10px 5px; }
|
||||
@@ -8,21 +10,21 @@ p { padding:0px; margin:5px 5px 10px 5px; }
|
||||
<body>
|
||||
<h2>Tenmon pomocník</h2>
|
||||
|
||||
<p>Tenmon slúži primárne na zobrazenie astronomických fotiek a obrázkov. Dokáže otvoriť nasledovné formáty:
|
||||
<p>Tenmon slúži primárne na zobrazenie astronomických fotiek a obrázkov. Dokáže otvoriť nasledovné formáty:</p>
|
||||
<ul>
|
||||
<li>FITS 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
|
||||
<li>XISF 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
|
||||
<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, NEF, DNG raw obrázky</li>
|
||||
<li>CR2, CR3, NEF, DNG raw obrázky</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Hlavné okno</h3>
|
||||
<p>V hlavnom okne sa zobrazujú načítané obrázky. Naľavo sú potom <i>Informácie o obrázku</i> kde sa zobrazujú podrobné
|
||||
informácie o aktuálnom obrázku a <i>Zoznam súborov</i> kde sú všetky obrázky z adresára kde je aktuálne zobrazený obrázok.
|
||||
Hore je hlavné menu a pod ním je <i>Panel úrovní</i>. Všetky panely sa dajú zavrieť a presúvať. Zatvorený panel sa dá znova
|
||||
zobraziť v menu <i>Dokovacie panely</i>.
|
||||
</p>
|
||||
zobraziť v menu <i>Dokovacie panely</i>.</p>
|
||||
<p>Na spodnom okraji okna je lišta v ktorej sa ukazuje aktuálna hodnota pixelu pod kurzorom a ak má obrázok WCS dátá aj aktuálne
|
||||
celestiálne koordináty.</p>
|
||||
|
||||
<h3>Otváranie a ukladanie obrázkov</h3>
|
||||
<p>Otvoriť obrázok je možné v menu <i>Súbor->Otvoriť</i>. Po vybraní súboru ktorý sa má otvoriť je
|
||||
@@ -43,15 +45,29 @@ hlavné okno na celú obrazovku. <i>Náhľady</i> zobrazí malé náhľady pre v
|
||||
<p>
|
||||
Tento panel umožňuje upraviť spôsob ako sa zobrazujú obrazové dáta. Ako prvá je na tomto panely posuvná škála
|
||||
na ktorej sa dajú nastaviť tri body.
|
||||
<br><br><img src=":/about/stretch-panel.png">
|
||||
<br><br><img src=":/about/stretch-panel.png"></p>
|
||||
<ul>
|
||||
<li>čierny bod - všetky pixeli s hodnotou menšou ako nastavená budú zobrazené ako čierne</li>
|
||||
<li>stredný bod - pixeli s touto hodnotou budú zobrazené ako 50% šedá</li>
|
||||
<li>biely bod - pixeli nad touto hodnotou budú zobrazené ako biele</li>
|
||||
</ul>
|
||||
<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.
|
||||
Super pixel CFA spriemeruje dva krát dva pixeli do jedného čo je vhodné pri prezeraní surových obrázkov z farebných kamier.
|
||||
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>
|
||||
@@ -75,9 +91,220 @@ Pre indexovanie nových súborov je treba znova pustiť indexáciu.</p>
|
||||
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>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,30 +0,0 @@
|
||||
#ifdef __linux__
|
||||
|
||||
#define QT_NO_KEYWORDS
|
||||
#include <QString>
|
||||
#include <iostream>
|
||||
#include <gio/gio.h>
|
||||
|
||||
//flatpak bug prevent to use QFile::moveToTrash
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
GFile *gfile = g_file_new_for_path(path.toLocal8Bit().data());
|
||||
GError *error = nullptr;
|
||||
g_file_trash(gfile, nullptr, &error);
|
||||
if(error)std::cerr << "failed to trash file " << error->code << " " << error->message << std::endl;
|
||||
g_clear_error(&error);
|
||||
g_object_unref(gfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
return QFile::moveToTrash(path);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,285 +0,0 @@
|
||||
#include "imageinfo.h"
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <QHeaderView>
|
||||
#include <wcslib/wcshdr.h>
|
||||
#include <wcslib/wcsfix.h>
|
||||
#include <libxisf.h>
|
||||
|
||||
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
|
||||
|
||||
bool FITSRecord::editable() const
|
||||
{
|
||||
return noEditableKey.count(key);
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment) :
|
||||
key(key), value(value), comment(comment)
|
||||
{
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
|
||||
{
|
||||
key = record.name.toUtf8();
|
||||
comment = record.comment.toUtf8();
|
||||
|
||||
QString string = record.value;
|
||||
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.toUtf8();
|
||||
value = property.value;
|
||||
comment = property.comment.toUtf8();
|
||||
}
|
||||
|
||||
QByteArray FITSRecord::valueToByteArray() const
|
||||
{
|
||||
if(value.type() == QVariant::Bool)
|
||||
return value.toBool() ? "T" : "F";
|
||||
else
|
||||
return value.toString().toLatin1();
|
||||
}
|
||||
|
||||
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
|
||||
{
|
||||
setColumnCount(3);
|
||||
setHeaderLabels({tr("Property"), tr("Value"), tr("Comment")});
|
||||
setIndentation(5);
|
||||
QSettings settings;
|
||||
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
|
||||
}
|
||||
|
||||
ImageInfo::~ImageInfo()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("imageinfo/headerstate", header()->saveState());
|
||||
}
|
||||
|
||||
void ImageInfo::setInfo(const ImageInfoData &info)
|
||||
{
|
||||
clear();
|
||||
if(info.fitsHeader.size())
|
||||
{
|
||||
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
|
||||
for(const FITSRecord &record : info.fitsHeader)
|
||||
{
|
||||
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString(), record.comment});
|
||||
}
|
||||
addTopLevelItem(fitsHeader);
|
||||
}
|
||||
if(info.info.size())
|
||||
{
|
||||
QTreeWidgetItem *infoHeader = new QTreeWidgetItem({tr("Image info")});
|
||||
for(auto &item : info.info)
|
||||
{
|
||||
new QTreeWidgetItem(infoHeader, {item.first, item.second});
|
||||
}
|
||||
addTopLevelItem(infoHeader);
|
||||
}
|
||||
expandAll();
|
||||
}
|
||||
|
||||
void WCSData::freeWCS()
|
||||
{
|
||||
wcsvfree(&nwcs, &wcs);
|
||||
nwcs = 0;
|
||||
wcs = nullptr;
|
||||
}
|
||||
|
||||
WCSData::WCSData(int width, int height, char *header, int nrec) :
|
||||
width(width),
|
||||
height(height)
|
||||
{
|
||||
int nreject = 0;
|
||||
int status = wcspih(header, nrec, 1, 0, &nreject, &nwcs, &wcs);
|
||||
if(status != 0)
|
||||
{
|
||||
freeWCS();
|
||||
return;
|
||||
}
|
||||
status = cdfix(wcs);
|
||||
if(status > 0 || wcs->crpix[0] == 0)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
WCSData::WCSData(int width, int height, const QVector<FITSRecord> &header) :
|
||||
width(width),
|
||||
height(height)
|
||||
{
|
||||
int status = 0;
|
||||
|
||||
QByteArray str;
|
||||
int nrec = 1;
|
||||
for(const FITSRecord &record : header)
|
||||
{
|
||||
if(record.key.startsWith("PV"))continue;
|
||||
|
||||
QByteArray rec;
|
||||
rec.append(record.key.leftJustified(8, ' '));
|
||||
rec.append("= ");
|
||||
rec.append(record.value.toString().toLatin1());
|
||||
rec.append(" / ");
|
||||
rec.append(record.comment);
|
||||
str.append(rec.leftJustified(80, ' ', true));
|
||||
nrec++;
|
||||
}
|
||||
str.append(QByteArray("END").leftJustified(80));
|
||||
|
||||
int nreject = 0;
|
||||
status = wcspih(str.data(), nrec, 1, 0, &nreject, &nwcs, &wcs);
|
||||
if(status != 0)
|
||||
{
|
||||
freeWCS();
|
||||
return;
|
||||
}
|
||||
status = cdfix(wcs);
|
||||
if(status > 0 || wcs->crpix[0] == 0)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
WCSData::~WCSData()
|
||||
{
|
||||
if(wcs)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
bool WCSData::pixelToWorld(const QPointF &pixel, SkyPoint &point) const
|
||||
{
|
||||
if(!valid())return false;
|
||||
|
||||
double pixcrd[2] = {pixel.x(), pixel.y()};
|
||||
double imgcrd[8] = {0};
|
||||
double phi = 0;
|
||||
double theta = 0;
|
||||
double world[8] = {0};
|
||||
int stat[NWCSFIX] = {0};
|
||||
int status = wcsp2s(wcs, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
|
||||
if(status == 0)
|
||||
{
|
||||
point = SkyPoint(world[0], world[1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WCSData::worldToPixel(const SkyPoint &point, QPointF &pixel) const
|
||||
{
|
||||
if(!valid())return false;
|
||||
|
||||
double world[2] = {point.RA(), point.DEC()};
|
||||
double phi = 0;
|
||||
double theta = 0;
|
||||
double imgcrd[8] = {0};
|
||||
double pixcrd[8] = {0};
|
||||
int stat[NWCSFIX] = {0};
|
||||
int status = wcss2p(wcs, 1, 2, world, &phi, &theta, imgcrd, pixcrd, stat);
|
||||
if(status == 0)
|
||||
{
|
||||
pixel = QPointF(pixcrd[0], pixcrd[1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WCSData::calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const
|
||||
{
|
||||
if(wcs == nullptr)return;
|
||||
|
||||
minRa = 1000;
|
||||
maxRa = -1000;
|
||||
minDec = 1000;
|
||||
maxDec = -1000;
|
||||
|
||||
if(wcs->crval)
|
||||
{
|
||||
crVal1 = wcs->crval[0];
|
||||
crVal2 = wcs->crval[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
crVal1 = crVal2 = NAN;
|
||||
}
|
||||
|
||||
auto update = [&](const QPointF &pixel)
|
||||
{
|
||||
SkyPoint point;
|
||||
pixelToWorld(pixel, point);
|
||||
minRa = std::min(minRa, point.RA());
|
||||
maxRa = std::max(maxRa, point.RA());
|
||||
minDec = std::min(minDec, point.DEC());
|
||||
maxDec = std::max(maxDec, point.DEC());
|
||||
};
|
||||
|
||||
for(int x=0; x<width; x++)
|
||||
{
|
||||
update(QPointF(x, 0));
|
||||
update(QPointF(x, height - 1));
|
||||
}
|
||||
|
||||
for(int y=0; y<height; y++)
|
||||
{
|
||||
update(QPointF(0, y));
|
||||
update(QPointF(width - 1, y));
|
||||
}
|
||||
|
||||
QPointF ncp;
|
||||
QPointF scp;
|
||||
QRectF s(0, 0, width - 1, height - 1);
|
||||
if(worldToPixel(SkyPoint(0, 90), ncp))
|
||||
{
|
||||
if(s.contains(ncp))
|
||||
maxDec = 90;
|
||||
}
|
||||
|
||||
if(worldToPixel(SkyPoint(0, -90), scp))
|
||||
{
|
||||
if(s.contains(scp))
|
||||
minDec = -90;
|
||||
}
|
||||
}
|
||||
|
||||
SkyPoint::SkyPoint() : ra(NAN), dec(NAN)
|
||||
{
|
||||
}
|
||||
|
||||
SkyPoint::SkyPoint(double ra, double dec) : ra(ra), dec(dec)
|
||||
{
|
||||
}
|
||||
|
||||
void SkyPoint::set(double ra, double dec)
|
||||
{
|
||||
this->ra = ra;
|
||||
this->dec = dec;
|
||||
}
|
||||
|
||||
QString SkyPoint::toString() const
|
||||
{
|
||||
if(std::isnan(ra) || std::isnan(dec))
|
||||
return QString();
|
||||
|
||||
QTime t(0, 0);
|
||||
t = t.addSecs(ra * 240);
|
||||
|
||||
double deg, min, sec;
|
||||
min = std::modf(dec, °) * 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');
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#ifndef IMAGEINFO_H
|
||||
#define IMAGEINFO_H
|
||||
|
||||
#include <QTreeWidget>
|
||||
#include <wcslib/wcs.h>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace LibXISF { struct FITSKeyword; struct Property; }
|
||||
|
||||
struct FITSRecord
|
||||
{
|
||||
QByteArray key;
|
||||
QVariant value;
|
||||
QByteArray comment;
|
||||
bool editable() const;
|
||||
FITSRecord(){}
|
||||
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
|
||||
FITSRecord(const LibXISF::FITSKeyword &record);
|
||||
FITSRecord(const LibXISF::Property &property);
|
||||
QByteArray valueToByteArray() const;
|
||||
};
|
||||
|
||||
class SkyPoint
|
||||
{
|
||||
double ra = NAN;
|
||||
double dec = NAN;
|
||||
public:
|
||||
SkyPoint();
|
||||
SkyPoint(double ra, double dec);
|
||||
void set(double ra, double dec);
|
||||
double RA() const { return ra; }
|
||||
double DEC() const { return dec; }
|
||||
QString toString() const;
|
||||
};
|
||||
|
||||
class WCSData
|
||||
{
|
||||
int nwcs = 0;
|
||||
struct wcsprm *wcs = nullptr;
|
||||
int width;
|
||||
int height;
|
||||
void freeWCS();
|
||||
public:
|
||||
WCSData(int width, int height, char *header, int nrec);
|
||||
WCSData(int width, int height, const QVector<FITSRecord> &header);
|
||||
WCSData(const WCSData &) = delete;
|
||||
~WCSData();
|
||||
bool pixelToWorld(const QPointF &pixel, SkyPoint &point) const;
|
||||
bool worldToPixel(const SkyPoint &point, QPointF &pixel) const;
|
||||
void calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const;
|
||||
bool valid() const { return wcs; };
|
||||
};
|
||||
|
||||
struct ImageInfoData
|
||||
{
|
||||
QVector<FITSRecord> fitsHeader;
|
||||
QVector<QPair<QString, QString>> info;
|
||||
std::shared_ptr<WCSData> wcs;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ImageInfoData);
|
||||
|
||||
typedef enum
|
||||
{
|
||||
None,
|
||||
Statistics,
|
||||
Peaks,
|
||||
Stars,
|
||||
}AnalyzeLevel;
|
||||
|
||||
class ImageInfo : public QTreeWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImageInfo(QWidget *parent);
|
||||
~ImageInfo() override;
|
||||
public slots:
|
||||
void setInfo(const ImageInfoData &info);
|
||||
};
|
||||
|
||||
#endif // IMAGEINFO_H
|
||||
@@ -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());
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#ifndef IMAGESCROLLAREA_H
|
||||
#define IMAGESCROLLAREA_H
|
||||
|
||||
#include <QScrollArea>
|
||||
#include <QLabel>
|
||||
|
||||
class ImageScrollArea : public QScrollArea
|
||||
{
|
||||
Q_OBJECT
|
||||
QPoint m_lastPos;
|
||||
QLabel *m_label;
|
||||
QPixmap m_pixmap;
|
||||
float m_scale;
|
||||
public:
|
||||
explicit ImageScrollArea(QWidget *parent = 0);
|
||||
void setImage(const QPixmap &img);
|
||||
void setScale(float scale);
|
||||
public slots:
|
||||
void zoomIn();
|
||||
void zoomOut();
|
||||
void bestFit();
|
||||
void oneToOne();
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void keyReleaseEvent(QKeyEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // IMAGESCROLLAREA_H
|
||||
@@ -1,842 +0,0 @@
|
||||
#include "imagescrollareagl.h"
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QDebug>
|
||||
#include <QKeyEvent>
|
||||
#include <QOpenGLDebugLogger>
|
||||
#include <QOpenGLPixelTransferOptions>
|
||||
#include <QOpenGLFramebufferObject>
|
||||
#include <QGridLayout>
|
||||
#include <QMimeData>
|
||||
#include <QMessageBox>
|
||||
#include <QCoreApplication>
|
||||
#include <QPainter>
|
||||
#include <QFileInfo>
|
||||
#include <cmath>
|
||||
|
||||
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},
|
||||
#ifdef COLOR_MANAGMENT
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::SRGB8, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGBA,QOpenGLTexture::SRGB8_Alpha8, QOpenGLTexture::UInt8, false},
|
||||
#else
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGBA,QOpenGLTexture::RGBA8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
#endif
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
|
||||
{QOpenGLTexture::RGBA, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
|
||||
};
|
||||
|
||||
static bool MANUAL_MIPMAP_GEN = 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(Database *database, QWidget *parent) : QOpenGLWidget(parent)
|
||||
, m_database(database)
|
||||
{
|
||||
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;
|
||||
m_showThumbnails = false;
|
||||
m_selecting = false;
|
||||
m_thumbnailCount = 0;
|
||||
m_updateTimer = new QTimer(this);
|
||||
m_updateTimer->setInterval(500);
|
||||
m_updateTimer->setSingleShot(true);
|
||||
m_sizesDirty = false;
|
||||
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(update()));
|
||||
setAcceptDrops(true);
|
||||
QTimer::singleShot(1000, [this](){
|
||||
if(!isValid())
|
||||
{
|
||||
QMessageBox::critical(this, tr("OpenGL error"), tr("Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed."));
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
});
|
||||
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
ImageWidget::~ImageWidget()
|
||||
{
|
||||
makeCurrent();
|
||||
}
|
||||
|
||||
void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
|
||||
{
|
||||
if(image == nullptr)return;
|
||||
makeCurrent();
|
||||
m_rawImage = image;
|
||||
m_rawImage->downscaleTo(m_maxTextureSize);
|
||||
|
||||
m_imgWidth = image->width();
|
||||
m_imgHeight = image->height();
|
||||
m_currentImg = index;
|
||||
m_whiteBalance[0] = m_whiteBalance[1] = m_whiteBalance[2] = 1.0f;
|
||||
|
||||
if(!m_image)return;
|
||||
|
||||
const RawImageType &rawImageType = rawImageTypes[image->type()];
|
||||
m_srgb = rawImageType.textureFormat == QOpenGLTexture::SRGB8 || rawImageType.textureFormat == QOpenGLTexture::SRGB8_Alpha8;
|
||||
m_bwImg = rawImageType.bw;
|
||||
|
||||
m_image->destroy();
|
||||
m_image->setAutoMipMapGenerationEnabled(false);
|
||||
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, (const void*)image->data(), m_transferOptions.get());
|
||||
|
||||
auto sRGB_linear = [](cv::Point3f &pixel, const int *pos)
|
||||
{
|
||||
pixel.x = pixel.x <= 0.04045f ? pixel.x / 12.92f : std::pow((pixel.x + 0.055) / 1.055f, 2.4f);
|
||||
pixel.y = pixel.y <= 0.04045f ? pixel.y / 12.92f : std::pow((pixel.y + 0.055) / 1.055f, 2.4f);
|
||||
pixel.z = pixel.z <= 0.04045f ? pixel.z / 12.92f : std::pow((pixel.z + 0.055) / 1.055f, 2.4f);
|
||||
};
|
||||
|
||||
auto linear_sRGB = [](cv::Point3f &pixel, const int *pos)
|
||||
{
|
||||
pixel.x = pixel.x <= 0.0031308f ? pixel.x * 12.92f : 1.055f * std::pow(pixel.x , 1/2.4f) - 0.055f;
|
||||
pixel.y = pixel.y <= 0.0031308f ? pixel.y * 12.92f : 1.055f * std::pow(pixel.y , 1/2.4f) - 0.055f;
|
||||
pixel.z = pixel.z <= 0.0031308f ? pixel.z * 12.92f : 1.055f * std::pow(pixel.z , 1/2.4f) - 0.055f;
|
||||
};
|
||||
|
||||
//AMD OpenGL driver on Windows doesn't generate mipmaps for sRGB textures correctly
|
||||
if(m_srgb && MANUAL_MIPMAP_GEN)
|
||||
{
|
||||
cv::Mat img = image->mat();
|
||||
img.convertTo(img, CV_32FC3, 1/255.0);
|
||||
img.forEach<cv::Point3f>(sRGB_linear);
|
||||
cv::Size size(img.cols, img.rows);
|
||||
for(int i=1; i<m_image->mipLevels(); i++)
|
||||
{
|
||||
cv::Mat mip;
|
||||
size /= 2;
|
||||
cv::resize(img, mip, size);
|
||||
mip.copyTo(img);
|
||||
mip.forEach<cv::Point3f>(linear_sRGB);
|
||||
mip.convertTo(mip, CV_8UC3, 255);
|
||||
m_image->setData(i, rawImageType.pixelFormat, rawImageType.dataType, (const void*)mip.ptr(), m_transferOptions.get());
|
||||
}
|
||||
}
|
||||
else m_image->generateMipMaps();
|
||||
|
||||
if(m_debayerTex)
|
||||
{
|
||||
f->glDeleteTextures(1, &m_debayerTex);
|
||||
m_debayerTex = 0;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::setWCS(std::shared_ptr<WCSData> wcs)
|
||||
{
|
||||
m_wcs = wcs;
|
||||
}
|
||||
|
||||
void ImageWidget::setScale(float scale)
|
||||
{
|
||||
m_scale = scale;
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::blockRepaint(bool block)
|
||||
{
|
||||
m_blockRepaint = block;
|
||||
if(!block)update();
|
||||
}
|
||||
|
||||
void ImageWidget::allocateThumbnails(const QStringList &paths)
|
||||
{
|
||||
makeCurrent();
|
||||
int count = paths.size();
|
||||
m_thumbnailCount = count;
|
||||
m_thumnails.clear();
|
||||
QStringList marked = m_database->getMarkedFiles();
|
||||
for(auto &path : paths)
|
||||
{
|
||||
QString name = QFileInfo(path).fileName();
|
||||
m_thumnails.push_back({name, path, QSize(0, 0), marked.contains(path), false});
|
||||
}
|
||||
|
||||
m_thumbnailTexture->destroy();
|
||||
m_thumbnailTexture->create();
|
||||
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
|
||||
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
|
||||
m_thumbnailTexture->setLayers(paths.size());
|
||||
m_thumbnailTexture->allocateStorage();
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.bindDefault();
|
||||
return fbo.toImage(true);
|
||||
}
|
||||
|
||||
void ImageWidget::thumbnailLoaded(const Image *image)
|
||||
{
|
||||
makeCurrent();
|
||||
const RawImage *raw = image->thumbnail();
|
||||
m_thumbnailTexture->setData(0, image->number(), QOpenGLTexture::RGB, QOpenGLTexture::UInt16, raw->data(), m_transferOptions.get());
|
||||
float a = raw->thumbAspect();
|
||||
int sizes[3] = { std::max(1, a > 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE * a)), std::max(1, a < 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE / a)), image->number() };
|
||||
m_sizesDirty = true;
|
||||
m_thumnails[image->number()].size = QSize(sizes[0], sizes[1]);
|
||||
if(!m_updateTimer->isActive())m_updateTimer->start();
|
||||
}
|
||||
|
||||
void ImageWidget::showThumbnail(bool enable)
|
||||
{
|
||||
m_showThumbnails = enable;
|
||||
update();
|
||||
}
|
||||
|
||||
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;
|
||||
QBrush highlight = style()->standardPalette().highlight();
|
||||
|
||||
if(m_showThumbnails)
|
||||
{
|
||||
m_vaoThumb->bind();
|
||||
m_thumbnailTexture->bind(1);
|
||||
if(m_sizesDirty)
|
||||
{
|
||||
m_bufferSizes->bind();
|
||||
int i = 0;
|
||||
std::vector<int> sizes(m_thumbnailCount*3);
|
||||
for(auto &s : m_thumnails)
|
||||
{
|
||||
sizes[3*i] = s.size.width();
|
||||
sizes[3*i+1] = s.size.height();
|
||||
sizes[3*i+2] = i;
|
||||
i++;
|
||||
}
|
||||
m_bufferSizes->allocate(&sizes[0], sizes.size()*sizeof(int));
|
||||
m_sizesDirty = false;
|
||||
}
|
||||
m_thumbnailProgram->bind();
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("viewport_row"), width(), height(), width()/THUMB_SIZE_BORDER);
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("thumb_size"), THUMB_SIZE_BORDER/2, THUMB_SIZE_BORDER, THUMB_SIZE_BORDER_Y);
|
||||
m_thumbnailProgram->setUniformValue("mtf_param", m_low, m_mid, m_high);
|
||||
m_thumbnailProgram->setUniformValue("invert", m_invert);
|
||||
m_thumbnailProgram->setUniformValue("offset", 0, m_dy);
|
||||
QMatrix4x4 mvp;
|
||||
mvp.ortho(rect());
|
||||
m_thumbnailProgram->setUniformValue("mvp", mvp);
|
||||
if(f3)f3->glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, m_thumbnailCount);
|
||||
|
||||
QPainter painter(this);
|
||||
const int w = width()/THUMB_SIZE_BORDER;
|
||||
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
|
||||
for(int i=0; i < m_thumbnailCount; i++)
|
||||
{
|
||||
float x = (i % w) * THUMB_SIZE_BORDER;
|
||||
float y = i / w * THUMB_SIZE_BORDER_Y + THUMB_SIZE - m_dy + off;
|
||||
QRectF rect(x, y, THUMB_SIZE_BORDER, 32);
|
||||
painter.drawText(rect, Qt::AlignCenter | Qt::TextWrapAnywhere, QString(m_thumnails[i].name));
|
||||
if(m_thumnails[i].selected)
|
||||
{
|
||||
painter.save();
|
||||
QRectF thumbRect;
|
||||
painter.setPen(Qt::red);
|
||||
thumbRect.setSize(m_thumnails[i].size);
|
||||
thumbRect.moveCenter(QPointF(x + THUMB_SIZE_BORDER / 2, y - THUMB_SIZE / 2));
|
||||
painter.drawRect(thumbRect);
|
||||
painter.restore();
|
||||
}
|
||||
if(m_currentImg == i)
|
||||
{
|
||||
painter.save();
|
||||
painter.setPen(QPen(highlight, 2.0));
|
||||
painter.drawRect((i % w) * THUMB_SIZE_BORDER + off, i / w * THUMB_SIZE_BORDER_Y - m_dy + off, THUMB_SIZE, THUMB_SIZE);
|
||||
painter.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
debayer();
|
||||
|
||||
m_vao->bind();
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
|
||||
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_superpixel);
|
||||
m_program->setUniformValue("invert", m_invert);
|
||||
if(m_superpixel)m_program->setUniformValue("whiteBalance", m_whiteBalance[0], m_whiteBalance[1], m_whiteBalance[2]);
|
||||
else m_program->setUniformValue("whiteBalance", 1.0f, 1.0f, 1.0f);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
m_program->setUniformValue("srgb", m_srgb);
|
||||
#endif
|
||||
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);
|
||||
f3 = context()->versionFunctions<QOpenGLFunctions_3_3_Core>();
|
||||
|
||||
if(f3 == nullptr)
|
||||
QMessageBox::critical(this, tr("OpenGL error"), tr("Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed."));
|
||||
|
||||
m_vao = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
|
||||
m_vaoThumb = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
|
||||
m_vao->create();
|
||||
m_vaoThumb->create();
|
||||
m_vao->bind();
|
||||
|
||||
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
|
||||
logger->initialize();
|
||||
logger->startLogging();
|
||||
connect(logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage &message)
|
||||
{
|
||||
qDebug() << message;
|
||||
});
|
||||
|
||||
qDebug() << "Vendor:" << (char*)f->glGetString(GL_VENDOR);
|
||||
qDebug() << "Renderer:" << (char*)f->glGetString(GL_RENDERER);
|
||||
qDebug() << "Version:" << (char*)f->glGetString(GL_VERSION);
|
||||
f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_maxTextureSize);
|
||||
f->glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &m_maxArrayLayers);
|
||||
qDebug() << "Max texture size:" << m_maxTextureSize << "max layers:" << m_maxArrayLayers;
|
||||
|
||||
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
|
||||
|
||||
qDebug() << context()->format();
|
||||
|
||||
// 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_debayerProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/debayer.vert");
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/debayer.frag");
|
||||
if(f3)f3->glBindFragDataLocation(m_debayerProgram->programId(), 0, "color");
|
||||
m_debayerProgram->bind();
|
||||
m_debayerProgram->enableAttributeArray("qt_Vertex");
|
||||
m_debayerProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
m_debayerProgram->enableAttributeArray("qt_MultiTexCoord0");
|
||||
m_debayerProgram->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
|
||||
m_debayerProgram->setUniformValue("qt_Texture0", (GLuint)0);
|
||||
if(!m_debayerProgram->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_debayerProgram->log();
|
||||
}
|
||||
|
||||
m_vaoThumb->bind();
|
||||
|
||||
m_thumbnailProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/thumb.vert");
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/thumb.frag");
|
||||
if(f3)f3->glBindFragDataLocation(m_thumbnailProgram->programId(), 0, "color");
|
||||
m_thumbnailProgram->bind();
|
||||
m_thumbnailProgram->enableAttributeArray("qt_Vertex");
|
||||
m_thumbnailProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
m_thumbnailProgram->enableAttributeArray("qt_MultiTexCoord0");
|
||||
m_thumbnailProgram->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
|
||||
if(!m_thumbnailProgram->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_thumbnailProgram->log();
|
||||
}
|
||||
m_thumbnailProgram->setUniformValue("qt_Texture0", (GLuint)1);
|
||||
|
||||
m_bufferSizes = std::unique_ptr<QOpenGLBuffer>(new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer));
|
||||
m_bufferSizes->setUsagePattern(QOpenGLBuffer::StaticDraw);
|
||||
m_bufferSizes->create();
|
||||
m_bufferSizes->bind();
|
||||
m_bufferSizes->allocate(12);
|
||||
|
||||
m_thumbnailProgram->enableAttributeArray("imageSize_num");
|
||||
f3->glVertexAttribIPointer(m_thumbnailProgram->attributeLocation("imageSize_num"), 3, GL_INT, 0, nullptr);
|
||||
f3->glVertexAttribDivisor(m_thumbnailProgram->attributeLocation("imageSize_num"), 1);
|
||||
|
||||
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_thumbnailTexture = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2DArray));
|
||||
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
|
||||
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
|
||||
m_thumbnailTexture->setLayers(1);
|
||||
m_thumbnailTexture->allocateStorage();
|
||||
m_thumbnailTexture->bind(1);
|
||||
m_thumbnailTexture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
|
||||
|
||||
m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
|
||||
m_transferOptions->setAlignment(1);
|
||||
|
||||
if(m_rawImage)
|
||||
setImage(m_rawImage, m_currentImg);
|
||||
}
|
||||
|
||||
void ImageWidget::dragEnterEvent(QDragEnterEvent *event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void ImageWidget::dropEvent(QDropEvent *event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
|
||||
{
|
||||
for(const QUrl &url : event->mimeData()->urls())
|
||||
{
|
||||
if(url.isLocalFile())
|
||||
{
|
||||
emit fileDropped(url.toLocalFile());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void ImageWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_showThumbnails && event->button() == Qt::LeftButton && event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier))
|
||||
m_selecting = true;
|
||||
|
||||
if(m_selecting)
|
||||
{
|
||||
thumbSelect(event);
|
||||
}
|
||||
else
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void ImageWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_selecting)
|
||||
{
|
||||
thumbSelect(event);
|
||||
}
|
||||
else
|
||||
event->ignore();
|
||||
|
||||
if(!m_showThumbnails && m_rawImage)
|
||||
{
|
||||
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;
|
||||
|
||||
QVector2D offset(dx, dy);
|
||||
QVector2D pos = QVector2D(event->pos());
|
||||
QVector2D pix = (pos + offset) / m_scale;
|
||||
QVector3D rgb;
|
||||
|
||||
SkyPoint sky;
|
||||
if(m_wcs)
|
||||
{
|
||||
m_wcs->pixelToWorld(QPointF(pix.x(), pix.y()), sky);
|
||||
}
|
||||
|
||||
if(m_rawImage->pixel(pix.x(), pix.y(), rgb))
|
||||
{
|
||||
if(m_bwImg)
|
||||
emit status(tr("L:%1").arg(rgb.x()), tr("X:%3 Y:%4").arg((int)pix.x()).arg((int)pix.y()), sky.toString());
|
||||
else
|
||||
emit status(tr("R:%1 G:%2 B:%3").arg(rgb.x()).arg(rgb.y()).arg(rgb.z()), tr("X:%3 Y:%4").arg((int)pix.x()).arg((int)pix.y()), sky.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_selecting)
|
||||
{
|
||||
m_selecting = false;
|
||||
event->accept();
|
||||
QStringList mark;
|
||||
QStringList unmark;
|
||||
for(auto &thumb : m_thumnails)
|
||||
{
|
||||
if(thumb.dirty)
|
||||
{
|
||||
if(thumb.selected)
|
||||
mark.append(thumb.path);
|
||||
else
|
||||
unmark.append(thumb.path);
|
||||
|
||||
thumb.dirty = false;
|
||||
}
|
||||
}
|
||||
if(!mark.isEmpty())
|
||||
m_database->mark(mark);
|
||||
if(!unmark.isEmpty())
|
||||
m_database->unmark(unmark);
|
||||
}
|
||||
else
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void ImageWidget::thumbSelect(QMouseEvent *event)
|
||||
{
|
||||
QPoint p = event->pos();
|
||||
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
|
||||
p.ry() += m_dy;
|
||||
const int w = width()/THUMB_SIZE_BORDER;
|
||||
int x = p.x() / THUMB_SIZE_BORDER;
|
||||
int y = p.y() / THUMB_SIZE_BORDER_Y;
|
||||
int i = y * w + x;
|
||||
event->accept();
|
||||
QRect rect(x * THUMB_SIZE_BORDER + off, y * THUMB_SIZE_BORDER_Y + off, THUMB_SIZE, THUMB_SIZE);
|
||||
if(x < w && i < m_thumbnailCount && rect.contains(p, true))
|
||||
{
|
||||
bool oldVal = m_thumnails[i].selected;
|
||||
bool newVal = oldVal;
|
||||
if(event->modifiers() == Qt::ShiftModifier)
|
||||
newVal = true;
|
||||
if(event->modifiers() == Qt::ControlModifier)
|
||||
newVal = false;
|
||||
|
||||
if(newVal != oldVal)
|
||||
{
|
||||
m_thumnails[i].selected = newVal;
|
||||
m_thumnails[i].dirty = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::debayer()
|
||||
{
|
||||
if(m_debayerTex > 0 || !m_superpixel || !m_bwImg || m_imgWidth < 0)return;
|
||||
|
||||
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight, QOpenGLFramebufferObject::NoAttachment, GL_TEXTURE_2D, GL_RGBA16);
|
||||
fbo.bind();
|
||||
|
||||
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
|
||||
|
||||
m_debayerProgram->bind();
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.release();
|
||||
f->glViewport(0, 0, m_width, m_height);
|
||||
m_debayerTex = fbo.takeTexture();
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
f->glGenerateMipmap(GL_TEXTURE_2D);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
int size = std::max(m_imgWidth, m_imgHeight);
|
||||
int level = 0;
|
||||
while(size >>= 1)level++;
|
||||
int w,h;
|
||||
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &w);
|
||||
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &h);
|
||||
uint16_t pixel[w*h*4];
|
||||
f3->glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_SHORT, pixel);
|
||||
float maxRGB = std::max(std::max(pixel[0], pixel[1]), pixel[2]);
|
||||
m_whiteBalance[0] = maxRGB / pixel[0];
|
||||
m_whiteBalance[1] = maxRGB / pixel[1];
|
||||
m_whiteBalance[2] = maxRGB / pixel[2];
|
||||
}
|
||||
|
||||
ImageScrollAreaGL::ImageScrollAreaGL(Database *database, QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
QGridLayout *layout = new QGridLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
m_imageWidget = new ImageWidget(database, this);
|
||||
|
||||
m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
|
||||
m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
|
||||
m_scale = 1.0f;
|
||||
m_bestFit = false;
|
||||
m_thumbCount = 0;
|
||||
|
||||
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(Image *image)
|
||||
{
|
||||
if(image && image->rawImage())
|
||||
{
|
||||
m_imageWidget->setImage(image->rawImage(), image->number());
|
||||
m_imageWidget->setWCS(image->info().wcs);
|
||||
m_imgWidth = image->rawImage()->width();
|
||||
m_imgHeight = image->rawImage()->height();
|
||||
if(m_bestFit)bestFit();
|
||||
updateScrollbars();
|
||||
}
|
||||
}
|
||||
|
||||
ImageWidget *ImageScrollAreaGL::imageWidget()
|
||||
{
|
||||
return m_imageWidget;
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::setThumbnails(int count)
|
||||
{
|
||||
m_thumbCount = count;
|
||||
if(m_thumbCount)
|
||||
{
|
||||
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
|
||||
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_verticalScrollBar->setPageStep(m_imageWidget->height());
|
||||
m_horizontalScrollBar->setPageStep(m_imageWidget->width());
|
||||
}
|
||||
updateScrollbars();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::updateScrollbars(bool zoom)
|
||||
{
|
||||
if(m_thumbCount)
|
||||
{
|
||||
m_horizontalScrollBar->hide();
|
||||
m_verticalScrollBar->show();
|
||||
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
|
||||
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
if(m_thumbCount)
|
||||
{
|
||||
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
|
||||
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_verticalScrollBar->setPageStep(m_imageWidget->height());
|
||||
m_horizontalScrollBar->setPageStep(m_imageWidget->width());
|
||||
}
|
||||
updateScrollbars();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QPoint delta = m_lastPos - event->pos();
|
||||
if(m_thumbCount == 0)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)
|
||||
{
|
||||
if(m_thumbCount)
|
||||
{
|
||||
m_verticalScrollBar->setValue(m_verticalScrollBar->value() - event->angleDelta().y());
|
||||
}
|
||||
else
|
||||
{
|
||||
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());
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
#ifndef IMAGESCROLLAREAGL_H
|
||||
#define IMAGESCROLLAREAGL_H
|
||||
|
||||
#include <memory>
|
||||
#include <QObject>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLFunctions_3_3_Core>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLTexture>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include "rawimage.h"
|
||||
#include "imageringlist.h"
|
||||
#include "database.h"
|
||||
|
||||
struct ImageThumb
|
||||
{
|
||||
QString name;
|
||||
QString path;
|
||||
QSize size;
|
||||
bool selected;
|
||||
bool dirty;
|
||||
};
|
||||
|
||||
class ImageWidget : public QOpenGLWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
QOpenGLFunctions *f;
|
||||
QOpenGLFunctions_3_3_Core *f3;
|
||||
QTimer *m_updateTimer;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_program;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_thumbnailProgram;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_debayerProgram;
|
||||
std::unique_ptr<QOpenGLBuffer> m_buffer;
|
||||
std::unique_ptr<QOpenGLBuffer> m_bufferSizes;
|
||||
std::unique_ptr<QOpenGLTexture> m_image;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vao;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vaoThumb;
|
||||
std::unique_ptr<QOpenGLPixelTransferOptions> m_transferOptions;
|
||||
std::unique_ptr<QOpenGLTexture> m_thumbnailTexture;
|
||||
GLuint m_debayerTex = 0;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::shared_ptr<WCSData> m_wcs;
|
||||
int m_width, m_height;
|
||||
int m_imgWidth, m_imgHeight;
|
||||
int m_currentImg;
|
||||
float m_low;
|
||||
float m_mid;
|
||||
float m_high;
|
||||
float m_range;
|
||||
float m_dx, m_dy;
|
||||
float m_scale;
|
||||
float m_whiteBalance[3] = {1.0f, 1.0f, 1.0f};
|
||||
bool m_blockRepaint;
|
||||
bool m_bwImg;
|
||||
bool m_invert;
|
||||
bool m_superpixel;
|
||||
bool m_showThumbnails;
|
||||
bool m_selecting;
|
||||
bool m_sizesDirty;
|
||||
bool m_srgb;
|
||||
int m_thumbnailCount;
|
||||
int m_maxTextureSize;
|
||||
int m_maxArrayLayers;
|
||||
QVector<ImageThumb> m_thumnails;
|
||||
Database *m_database;
|
||||
public:
|
||||
explicit ImageWidget(Database *database, QWidget *parent = nullptr);
|
||||
~ImageWidget() override;
|
||||
void setImage(std::shared_ptr<RawImage> image, int index);
|
||||
void setImage(const QPixmap &pixmap);
|
||||
void setWCS(std::shared_ptr<WCSData> wcs);
|
||||
void setScale(float scale);
|
||||
void blockRepaint(bool block);
|
||||
void allocateThumbnails(const QStringList &paths);
|
||||
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();
|
||||
void thumbnailLoaded(const Image *image);
|
||||
void showThumbnail(bool enable);
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void initializeGL() override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void thumbSelect(QMouseEvent *event);
|
||||
void debayer();
|
||||
signals:
|
||||
void fileDropped(const QString &path);
|
||||
void status(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
|
||||
};
|
||||
|
||||
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;
|
||||
int m_thumbCount;
|
||||
public:
|
||||
explicit ImageScrollAreaGL(Database *database, QWidget *parent = nullptr);
|
||||
~ImageScrollAreaGL() override;
|
||||
void setImage(Image *image);
|
||||
ImageWidget* imageWidget();
|
||||
void setThumbnails(int count);
|
||||
protected:
|
||||
void updateScrollbars(bool zoom = false);
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void zoom(float delta);
|
||||
public slots:
|
||||
void zoomIn();
|
||||
void zoomOut();
|
||||
void bestFit();
|
||||
void oneToOne();
|
||||
protected slots:
|
||||
void scrollEvent();
|
||||
};
|
||||
|
||||
#endif // IMAGESCROLLAREAGL_H
|
||||
@@ -1,5 +1,7 @@
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
find_program(XDG-ICON-RESOURCE_EXECUTABLE xdg-icon-resource)
|
||||
find_program(XDG-MIME xdg-mime)
|
||||
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor space.nouspiro.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 resources/space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 resources/space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-MIME} install --novendor space.nouspiro.tenmon.xisf.xml WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
@@ -1,689 +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 <libxisf.h>
|
||||
#include "rawimage.h"
|
||||
#include "starfit.h"
|
||||
#include "wcslib/wcshdr.h"
|
||||
|
||||
#ifdef COLOR_MANAGMENT
|
||||
#include <QColorSpace>
|
||||
#endif
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
|
||||
m_file(file),
|
||||
m_receiver(receiver),
|
||||
m_analyzeLevel(level),
|
||||
m_thumbnail(thumbnail)
|
||||
{
|
||||
}
|
||||
|
||||
void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag)
|
||||
{
|
||||
char val[1024];
|
||||
ExifEntry *entry = exif_content_get_entry(content, tag);
|
||||
if(entry)
|
||||
{
|
||||
exif_entry_get_value(entry, val, sizeof(val));
|
||||
info.info.append({exif_tag_get_title(tag), val});
|
||||
}
|
||||
}
|
||||
|
||||
void drawPeaks(QImage &img, const std::vector<Peak> &peaks)
|
||||
{
|
||||
QPixmap pix = QPixmap::fromImage(img);
|
||||
QPainter painter(&pix);
|
||||
painter.setPen(Qt::red);
|
||||
for(auto peak : peaks)
|
||||
{
|
||||
painter.drawEllipse(QPoint(peak.x(), peak.y()), 5, 5);
|
||||
}
|
||||
img = pix.toImage();
|
||||
}
|
||||
|
||||
void drawStars(QImage &img, const std::vector<Star> &stars)
|
||||
{
|
||||
QPixmap pix = QPixmap::fromImage(img);
|
||||
QPainter painter(&pix);
|
||||
painter.setPen(Qt::red);
|
||||
for(auto star : stars)
|
||||
{
|
||||
painter.drawEllipse(QPointF(star.m_x, star.m_y), star.hw20X(), star.hw20Y());
|
||||
}
|
||||
img = pix.toImage();
|
||||
}
|
||||
|
||||
void printStarModel(int radius, const std::vector<double> &data, const Star &star)
|
||||
{
|
||||
QString d = "d=[";
|
||||
QString m = "m=[";
|
||||
for(int y=0; y<radius; y++)
|
||||
{
|
||||
for(int x=0; x<radius; x++)
|
||||
{
|
||||
d += QString::number(data[y*radius+x]) + ",";
|
||||
m += QString::number(gauss_model(star.m_am, star.m_x, star.m_y, star.m_sx, star.m_sy, x, y)) + ",";
|
||||
}
|
||||
d += ";";
|
||||
m += ";";
|
||||
}
|
||||
d += "];";
|
||||
m += "];";
|
||||
//std::cout << star.m_am << " " << star.m_sx << star.m_sy << std::endl;
|
||||
std::cout << d.toStdString() << std::endl;
|
||||
std::cout << m.toStdString() << std::endl << std::endl;
|
||||
}
|
||||
|
||||
bool loadRAW(const QString path, ImageInfoData &info, RawImage **image)
|
||||
{
|
||||
if(!image)
|
||||
return false;
|
||||
|
||||
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
|
||||
raw->open_file(path.toLocal8Bit().data());
|
||||
raw->imgdata.params.half_size = true;
|
||||
raw->imgdata.params.use_camera_wb = true;
|
||||
raw->imgdata.params.user_flip = 0;
|
||||
if(raw->unpack())
|
||||
return false;
|
||||
|
||||
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 imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
int nexist;
|
||||
int status = 0;
|
||||
char key[FLEN_KEYWORD];
|
||||
char val[FLEN_VALUE];
|
||||
char comm[FLEN_COMMENT];
|
||||
char strval[FLEN_VALUE];
|
||||
QVariant var;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
||||
fits_get_hdrspace(file, &nexist, nullptr, &status);
|
||||
for(int i=1; i<=nexist; i++)
|
||||
{
|
||||
fits_read_keyn(file, i, key, val, comm, &status);
|
||||
fits_read_key(file, TSTRING, key, strval, nullptr, &status);
|
||||
if(status == 0 || status == VALUE_UNDEFINED)
|
||||
{
|
||||
QString string(strval);
|
||||
bool isint;
|
||||
bool isdouble;
|
||||
double vald = string.toDouble(&isdouble);
|
||||
long long vall = string.toLongLong(&isint);
|
||||
if(isint)
|
||||
var = vall;
|
||||
else if(isdouble)
|
||||
var = vald;
|
||||
else if(status == VALUE_UNDEFINED)
|
||||
var = QVariant();
|
||||
else if(string == "T" || string == "F")
|
||||
var = string == "T";
|
||||
else
|
||||
var = string;
|
||||
status = 0;
|
||||
info.fitsHeader.append(FITSRecord(key, var, comm));
|
||||
}
|
||||
else
|
||||
{
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
char *header = nullptr;
|
||||
int nrec = 0;
|
||||
const char *exclist[] = {"PV1_1", "PV1_2"};
|
||||
fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
|
||||
if(status == 0)
|
||||
{
|
||||
info.wcs = std::make_shared<WCSData>(naxes[0], naxes[1], header, nrec);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
fits_free_memory(header, &status);
|
||||
return status;
|
||||
}
|
||||
|
||||
bool loadFITS(const QString path, ImageInfoData &info, 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);
|
||||
fits_get_img_equivtype(file, &imgtype, &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_16S;
|
||||
fitstype = TSHORT;
|
||||
break;
|
||||
case USHORT_IMG:
|
||||
cvtype = CV_16U;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case FLOAT_IMG:
|
||||
cvtype = CV_32F;
|
||||
fitstype = TFLOAT;
|
||||
break;
|
||||
default:
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
goto noload;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t size = naxes[0]*naxes[1];
|
||||
size_t w = naxes[0];
|
||||
size_t h = naxes[1];
|
||||
|
||||
info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
|
||||
info.info.append({QObject::tr("Height"), QString::number(naxes[1])});
|
||||
|
||||
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);
|
||||
if(cvtype == CV_16S)
|
||||
tmp.convertTo(tmp, CV_16U, 1, 32767);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
noload:
|
||||
if(file)
|
||||
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() << "Failed to load FITS file" << err;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path);
|
||||
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(0);
|
||||
|
||||
auto fitskeywords = xisfImage.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
auto imageproperties = xisfImage.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
|
||||
info.wcs = std::make_shared<WCSData>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
|
||||
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
|
||||
if(xisfImage.channelCount() == 1)
|
||||
{
|
||||
switch(xisfImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8:
|
||||
*image = new RawImage(xisfImage.width(), xisfImage.height(), RawImage::UINT8);
|
||||
std::memcpy((*image)->data(), xisfImage.imageData(), xisfImage.imageDataSize());
|
||||
break;
|
||||
case LibXISF::Image::UInt16:
|
||||
*image = new RawImage(xisfImage.width(), xisfImage.height(), RawImage::UINT16);
|
||||
std::memcpy((*image)->data(), xisfImage.imageData(), xisfImage.imageDataSize());
|
||||
break;
|
||||
case LibXISF::Image::Float32:
|
||||
*image = new RawImage(xisfImage.width(), xisfImage.height(), RawImage::FLOAT32);
|
||||
std::memcpy((*image)->data(), xisfImage.imageData(), xisfImage.imageDataSize());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(xisfImage.channelCount() == 3)
|
||||
{
|
||||
LibXISF::Image tmpImage = xisfImage;
|
||||
tmpImage.convertPixelStorageTo(LibXISF::Image::Normal);
|
||||
|
||||
switch(tmpImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8:
|
||||
*image = new RawImage(tmpImage.width(), tmpImage.height(), RawImage::UINT8C3);
|
||||
std::memcpy((*image)->data(), tmpImage.imageData(), tmpImage.imageDataSize());
|
||||
break;
|
||||
case LibXISF::Image::UInt16:
|
||||
*image = new RawImage(tmpImage.width(), tmpImage.height(), RawImage::UINT16C3);
|
||||
std::memcpy((*image)->data(), tmpImage.imageData(), tmpImage.imageDataSize());
|
||||
break;
|
||||
case LibXISF::Image::Float32:
|
||||
*image = new RawImage(tmpImage.width(), tmpImage.height(), RawImage::FLOAT32C3);
|
||||
std::memcpy((*image)->data(), tmpImage.imageData(), tmpImage.imageDataSize());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(*image)
|
||||
return true;
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
info.info.append(QPair<QString, QString>("Error", err.what()));
|
||||
qDebug() << "Failed to load XISF" << err.what();
|
||||
return false;
|
||||
}
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
return false;
|
||||
}
|
||||
|
||||
void LoadRunable::run()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if(!m_thumbnail && !m_receiver->isCurrent())
|
||||
{
|
||||
return;
|
||||
}
|
||||
QElapsedTimer timer;
|
||||
ImageInfoData info;
|
||||
QFileInfo finfo(m_file);
|
||||
info.info.append({QObject::tr("Filename"), finfo.fileName()});
|
||||
|
||||
RawImage *rawImage = nullptr;
|
||||
bool raw = false;
|
||||
timer.start();
|
||||
if(m_file.endsWith(".CR2", Qt::CaseInsensitive) || m_file.endsWith(".NEF", Qt::CaseInsensitive) || m_file.endsWith(".DNG", Qt::CaseInsensitive))
|
||||
{
|
||||
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);
|
||||
qDebug() << "LoadFITS" << timer.elapsed();
|
||||
}
|
||||
else if(m_file.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
{
|
||||
loadXISF(m_file, info, &rawImage);
|
||||
qDebug() << "LoadXISF" << timer.elapsed();
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img(m_file);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb)
|
||||
img.convertToColorSpace(QColorSpace::SRgb);
|
||||
#endif
|
||||
|
||||
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);
|
||||
qDebug() << "LoadQImage" << timer.elapsed();
|
||||
}
|
||||
|
||||
if(rawImage && m_analyzeLevel >= Statistics && !m_thumbnail)
|
||||
{
|
||||
double mean, median, min, max, mad;
|
||||
double stdDev;
|
||||
uint32_t saturated;
|
||||
timer.start();
|
||||
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad, &saturated);
|
||||
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)});
|
||||
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * saturated / rawImage->size()) + "%"});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
if(m_thumbnail)
|
||||
{
|
||||
if(rawImage)
|
||||
{
|
||||
rawImage->convertToThumbnail();
|
||||
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(void*, rawImage));
|
||||
}
|
||||
}
|
||||
else
|
||||
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(void*, rawImage), Q_ARG(ImageInfoData, info));
|
||||
}
|
||||
catch(cv::Exception e)
|
||||
{
|
||||
qDebug() << e.what();
|
||||
}
|
||||
catch(std::exception e)
|
||||
{
|
||||
qDebug() << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
fitsfile *fr;
|
||||
int status = 0;
|
||||
fits_open_diskfile(&fr, path.toLocal8Bit().data(), READONLY, &status);
|
||||
|
||||
if(fr && status == 0)
|
||||
{
|
||||
status = loadFITSHeader(fr, info);
|
||||
fits_close_file(fr, &status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path);
|
||||
const LibXISF::Image &image = xisf.getImage(0, false);
|
||||
|
||||
auto fitskeywords = image.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
|
||||
auto imageproperties = image.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
info.wcs = std::make_shared<WCSData>(image.width(), image.height(), info.fitsHeader);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << err.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format) :
|
||||
m_infile(in),
|
||||
m_outfile(out),
|
||||
m_format(format)
|
||||
{
|
||||
}
|
||||
|
||||
void writeFITSImage(fitsfile *fw, RawImage *rawimage, ImageInfoData &imageinfo)
|
||||
{
|
||||
static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"};
|
||||
|
||||
int status = 0;
|
||||
long firstpix[3] = {1,1,1};
|
||||
|
||||
int channels = rawimage->mat().channels();
|
||||
int naxis = channels == 1 ? 2 : 3;
|
||||
long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->mat().channels()};
|
||||
|
||||
std::vector<cv::Mat> mat;
|
||||
if(channels == 1)
|
||||
mat.push_back(rawimage->mat());
|
||||
else
|
||||
cv::split(rawimage->mat(), mat);
|
||||
|
||||
switch(CV_MAT_DEPTH(rawimage->dataType()))
|
||||
{
|
||||
case CV_8U:
|
||||
fits_create_img(fw, BYTE_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TBYTE, firstpix, rawimage->size(), mat[i].data, &status);
|
||||
}
|
||||
break;
|
||||
case CV_16U:
|
||||
fits_create_img(fw, USHORT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TUSHORT, firstpix, rawimage->size(), mat[i].data, &status);
|
||||
}
|
||||
break;
|
||||
case CV_32F:
|
||||
fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TFLOAT, firstpix, rawimage->size(), mat[i].data, &status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
for(const FITSRecord &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(skipKeys.contains(record.key))continue;
|
||||
|
||||
bool isdouble;
|
||||
bool isint;
|
||||
bool isbool = record.value.toString() == "T" || record.value.toString() == "F";
|
||||
double vald = record.value.toDouble(&isdouble);
|
||||
int valb = record.value.toString() == "T";
|
||||
long long vall = record.value.toLongLong(&isint);
|
||||
QByteArray str = record.value.toString().toLatin1();
|
||||
if(isdouble)
|
||||
fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isint)
|
||||
fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isbool)
|
||||
fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "COMMENT")
|
||||
fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "HISTORY")
|
||||
fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else
|
||||
fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
}
|
||||
}
|
||||
|
||||
void ConvertRunable::run()
|
||||
{
|
||||
ImageInfoData imageinfo;
|
||||
RawImage *rawimage = nullptr;
|
||||
if(m_infile.endsWith(".FITS", Qt::CaseInsensitive) || m_infile.endsWith(".FIT", Qt::CaseInsensitive))
|
||||
loadFITS(m_infile, imageinfo, &rawimage);
|
||||
if(m_infile.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
loadXISF(m_infile, imageinfo, &rawimage);
|
||||
|
||||
if(rawimage)
|
||||
{
|
||||
if(m_format == "XISF")
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFWriter xisf;
|
||||
int channelCount = rawimage->mat().channels();
|
||||
LibXISF::Image::SampleFormat sampleFormat;
|
||||
switch(CV_MAT_DEPTH(rawimage->dataType()))
|
||||
{
|
||||
case CV_8U: sampleFormat = LibXISF::Image::UInt8; break;
|
||||
case CV_16U: sampleFormat = LibXISF::Image::UInt16; break;
|
||||
case CV_32F: sampleFormat = LibXISF::Image::Float32; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Normal);
|
||||
std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize());
|
||||
for(auto &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(record.value.type() == QVariant::Bool)
|
||||
image.addFITSKeyword({record.key, record.value.toBool() ? "T" : "F", record.comment});
|
||||
else
|
||||
image.addFITSKeyword({record.key, record.value.toString(), record.comment});
|
||||
}
|
||||
|
||||
xisf.writeImage(image);
|
||||
xisf.save(m_outfile);
|
||||
}
|
||||
catch(LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << "Failed to save XISF image" << err.what();
|
||||
delete rawimage;
|
||||
}
|
||||
}
|
||||
|
||||
if(m_format == "FITS")
|
||||
{
|
||||
int status = 0;
|
||||
fitsfile *fw;
|
||||
if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile);
|
||||
fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status);
|
||||
writeFITSImage(fw, rawimage, imageinfo);
|
||||
fits_close_file(fw, &status);
|
||||
}
|
||||
delete rawimage;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#ifndef LOADRUNABLE_H
|
||||
#define LOADRUNABLE_H
|
||||
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
#include "imageinfo.h"
|
||||
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info);
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info);
|
||||
|
||||
class Image;
|
||||
|
||||
class LoadRunable : public QRunnable
|
||||
{
|
||||
QString m_file;
|
||||
Image *m_receiver;
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
bool m_thumbnail;
|
||||
public:
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail = false);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
class ConvertRunable : public QRunnable
|
||||
{
|
||||
QString m_infile;
|
||||
QString m_outfile;
|
||||
QString m_format;
|
||||
public:
|
||||
ConvertRunable(const QString &in, const QString &out, const QString &format);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
#endif // LOADRUNABLE_H
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QTranslator>
|
||||
#include <stdlib.h>
|
||||
#include "libxisf.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifdef __linux__
|
||||
setenv("LC_NUMERIC", "C", 1);
|
||||
#endif
|
||||
|
||||
QSurfaceFormat format;
|
||||
format.setMajorVersion(3);
|
||||
format.setMinorVersion(3);
|
||||
//format.setOption(QSurfaceFormat::DebugContext);
|
||||
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setOrganizationName("nou");
|
||||
a.setApplicationName("Tenmon");
|
||||
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
|
||||
|
||||
QTranslator translator;
|
||||
QTranslator translator2;
|
||||
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
|
||||
a.installTranslator(&translator);
|
||||
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
|
||||
a.installTranslator(&translator2);
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
#include "rawimage.h"
|
||||
|
||||
int THUMB_SIZE = 128;
|
||||
int THUMB_SIZE_BORDER = 138;
|
||||
int THUMB_SIZE_BORDER_Y = 158;
|
||||
double SATURATION = 0.95;
|
||||
|
||||
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_16UC4:
|
||||
return RawImage::UINT16C4;
|
||||
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::UINT16C4:
|
||||
return CV_16UC4;
|
||||
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;
|
||||
scaleToUnit();
|
||||
}
|
||||
|
||||
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;
|
||||
m_saturated = d.m_saturated;
|
||||
}
|
||||
|
||||
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);
|
||||
cv::cvtColor(m_img, m_img, cv::COLOR_BGRA2RGB);
|
||||
}
|
||||
else if(img.format() == QImage::Format_ARGB32)
|
||||
{
|
||||
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);
|
||||
cv::cvtColor(m_img, m_img, cv::COLOR_BGRA2RGBA);
|
||||
}
|
||||
else if(img.format() == QImage::Format_RGBX64)
|
||||
{
|
||||
m_img.create(img.height(), img.width(), CV_16UC4);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*8);
|
||||
cv::cvtColor(m_img, m_img, cv::COLOR_RGBA2RGB);
|
||||
}
|
||||
else if(img.format() == QImage::Format_RGBA64)
|
||||
{
|
||||
m_img.create(img.height(), img.width(), CV_16UC4);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*8);
|
||||
}
|
||||
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, uint32_t *saturated)
|
||||
{
|
||||
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;
|
||||
if(saturated)*saturated = m_saturated;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if(img.type() == CV_32F)m_median /= histSize;
|
||||
|
||||
int threshold = SATURATION * histSize;
|
||||
m_saturated = 0;
|
||||
for(int i = histSize-1; i >= threshold; i--)
|
||||
m_saturated += hist.at<float>(0, i);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if(img.type() == CV_32F)m_mad /= histSize;
|
||||
}
|
||||
|
||||
void RawImage::rect(int &x, int &y, int w, int h, std::vector<double> &r) const
|
||||
{
|
||||
r.resize(w*h);
|
||||
x -= w/2;
|
||||
y -= h/2;
|
||||
if(x<0)x = 0;
|
||||
if(y<0)y = 0;
|
||||
if(x+w >= m_img.cols)x = m_img.cols-w;
|
||||
if(y+h >= m_img.rows)y = m_img.rows-h;
|
||||
cv::Mat roiImg(m_img, cv::Rect(x, y, w, h));
|
||||
cv::Mat doubleMat;
|
||||
roiImg.convertTo(doubleMat, CV_64F);
|
||||
r = std::vector<double>(doubleMat.begin<double>(), doubleMat.end<double>());
|
||||
}
|
||||
|
||||
int RawImage::findPeaks(double background, double distance, std::vector<Peak> &peaks) const
|
||||
{
|
||||
std::vector<std::vector<cv::Point>> contours;
|
||||
|
||||
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(distance, distance));
|
||||
|
||||
cv::Mat img, mask, dilate, locMax, result;
|
||||
if(m_img.channels() == 1)img = m_img;
|
||||
else cv::cvtColor(m_img, img, cv::COLOR_RGB2GRAY);
|
||||
|
||||
cv::dilate(img, dilate, kernel);
|
||||
cv::compare(img, dilate, locMax, cv::CMP_GE);
|
||||
cv::compare(img, cv::Scalar(background), mask, cv::CMP_GT);
|
||||
cv::bitwise_and(locMax, mask, result);
|
||||
|
||||
cv::findContours(result, contours, cv::noArray(), cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
|
||||
peaks.reserve(contours.size());
|
||||
for(auto contour : contours)
|
||||
{
|
||||
peaks.push_back(Peak(1, contour[0].x, contour[0].y));
|
||||
}
|
||||
|
||||
return peaks.size();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
int RawImage::dataType() const
|
||||
{
|
||||
return 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();
|
||||
}
|
||||
|
||||
void RawImage::convertToThumbnail()
|
||||
{
|
||||
m_thumbAspect = (float)width() / height();
|
||||
switch(CV_MAT_DEPTH(m_img.type()))
|
||||
{
|
||||
case CV_8U:
|
||||
m_img.convertTo(m_img, CV_16U, 255);
|
||||
break;
|
||||
case CV_32F:
|
||||
m_img.convertTo(m_img, CV_16U, 65535);
|
||||
break;
|
||||
case CV_16U:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(m_img.channels() == 1)
|
||||
cv::cvtColor(m_img, m_img, cv::COLOR_GRAY2RGB);
|
||||
if(m_img.channels() == 4)
|
||||
cv::cvtColor(m_img, m_img, cv::COLOR_RGBA2RGB);
|
||||
cv::Size dsize(THUMB_SIZE, THUMB_SIZE);
|
||||
cv::resize(m_img, m_img, dsize, 0, 0, cv::INTER_NEAREST);
|
||||
}
|
||||
|
||||
float RawImage::thumbAspect() const
|
||||
{
|
||||
return m_thumbAspect;
|
||||
}
|
||||
|
||||
const cv::Mat& RawImage::mat() const
|
||||
{
|
||||
return m_img;
|
||||
}
|
||||
|
||||
bool RawImage::pixel(int x, int y, QVector3D &rgb) const
|
||||
{
|
||||
if(x < 0 || y < 0 || x >= (int)width() || y >= (int)height())return false;
|
||||
|
||||
switch(m_img.type())
|
||||
{
|
||||
case CV_8U:
|
||||
{
|
||||
uint8_t v = m_img.at<uint8_t>(y, x);
|
||||
rgb = QVector3D(v, v, v);
|
||||
break;
|
||||
}
|
||||
case CV_16U:
|
||||
{
|
||||
uint16_t v = m_img.at<uint16_t>(y, x);
|
||||
rgb = QVector3D(v, v, v);
|
||||
break;
|
||||
}
|
||||
case CV_32F:
|
||||
{
|
||||
float v = m_img.at<float>(y, x);
|
||||
rgb = QVector3D(v, v, v);
|
||||
break;
|
||||
}
|
||||
case CV_8UC3:
|
||||
{
|
||||
cv::Vec3b v = m_img.at<cv::Vec3b>(y, x);
|
||||
rgb = QVector3D(v[0], v[1], v[2]);
|
||||
break;
|
||||
}
|
||||
case CV_8UC4:
|
||||
{
|
||||
cv::Vec4b v = m_img.at<cv::Vec4b>(y, x);
|
||||
rgb = QVector3D(v[0], v[1], v[2]);
|
||||
break;
|
||||
}
|
||||
case CV_16UC3:
|
||||
{
|
||||
cv::Vec3w v = m_img.at<cv::Vec3w>(y, x);
|
||||
rgb = QVector3D(v[0], v[1], v[2]);
|
||||
break;
|
||||
}
|
||||
case CV_32FC3:
|
||||
{
|
||||
cv::Vec3f v = m_img.at<cv::Vec3f>(y, x);
|
||||
rgb = QVector3D(v[0], v[1], v[2]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
rgb = QVector3D(0, 0, 0);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RawImage::scaleToUnit()
|
||||
{
|
||||
if(CV_MAT_DEPTH(m_img.type()) == CV_32F)
|
||||
{
|
||||
double min, max;
|
||||
cv::minMaxIdx(m_img, &min, &max);
|
||||
if(min < 0 || max > 1)
|
||||
{
|
||||
float scale = 1.0 / (max - min);
|
||||
float zero = min * scale;
|
||||
m_img = m_img * scale - zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RawImage::downscaleTo(uint32_t size)
|
||||
{
|
||||
if(size < width() || size < height())
|
||||
{
|
||||
double s = (double)size / std::max(width(), height());
|
||||
cv::Size dsize(std::floor(width() * s), std::floor(height() * s));
|
||||
cv::resize(m_img, m_img, dsize, 0, 0, cv::INTER_AREA);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +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>
|
||||
#include <QVector3D>
|
||||
|
||||
extern int THUMB_SIZE;
|
||||
extern int THUMB_SIZE_BORDER;
|
||||
extern int THUMB_SIZE_BORDER_Y;
|
||||
|
||||
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;
|
||||
float m_thumbAspect;
|
||||
uint32_t m_saturated;
|
||||
public:
|
||||
enum ImgType
|
||||
{
|
||||
UINT8,
|
||||
UINT16,
|
||||
FLOAT32,
|
||||
UINT8C3,
|
||||
UINT8C4,
|
||||
UINT16C3,
|
||||
UINT16C4,
|
||||
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, uint32_t *saturated);
|
||||
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;
|
||||
int dataType() const;
|
||||
uint32_t norm() const;
|
||||
void* data();
|
||||
const void* data() const;
|
||||
void convertToThumbnail();
|
||||
float thumbAspect() const;
|
||||
const cv::Mat& mat() const;
|
||||
bool pixel(int x, int y, QVector3D &rgb) const;
|
||||
void scaleToUnit();
|
||||
void downscaleTo(uint32_t size);
|
||||
};
|
||||
|
||||
#endif // RAWIMAGE_H
|
||||
@@ -1,30 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>invert.png</file>
|
||||
<file>nuke.png</file>
|
||||
<file>bayer.png</file>
|
||||
<file>space.nouspiro.tenmon.png</file>
|
||||
<file>nuke_a.png</file>
|
||||
<file>about/tenmon</file>
|
||||
<file>translations/tenmon_en.qm</file>
|
||||
<file>translations/tenmon_sk.qm</file>
|
||||
<file>about/filter.png</file>
|
||||
<file>about/stretch-panel.png</file>
|
||||
<file>translations/tenmon_fr.qm</file>
|
||||
<file>shaders/image.frag</file>
|
||||
<file>shaders/image.vert</file>
|
||||
<file>shaders/thumb.frag</file>
|
||||
<file>shaders/thumb.vert</file>
|
||||
<file>shaders/debayer.frag</file>
|
||||
<file>shaders/debayer.vert</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>
|
||||
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 947 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 316 B |
@@ -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 |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B |
@@ -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 |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,109 +0,0 @@
|
||||
#include "settingsdialog.h"
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include "rawimage.h"
|
||||
|
||||
extern int DEFAULT_WIDTH;
|
||||
extern double SATURATION;
|
||||
|
||||
class EvenNumber : public QSpinBox
|
||||
{
|
||||
public:
|
||||
explicit EvenNumber(QWidget *parent) : QSpinBox(parent){}
|
||||
protected:
|
||||
QValidator::State validate(QString &text, int &) const
|
||||
{
|
||||
bool ok;
|
||||
int val = text.toInt(&ok);
|
||||
if(ok && (val & 1) == 0)return QValidator::Acceptable;
|
||||
if(ok)return QValidator::Intermediate;
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
void fixup(QString &input) const
|
||||
{
|
||||
bool ok;
|
||||
int val = input.toInt(&ok);
|
||||
val -= val & 1;
|
||||
input = QString::number(val);
|
||||
}
|
||||
};
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
QFormLayout *layout = new QFormLayout(this);
|
||||
setWindowTitle(tr("Settings"));
|
||||
|
||||
QSettings settings;
|
||||
|
||||
m_preloadImages = new QSpinBox(this);
|
||||
m_preloadImages->setRange(0, 8);
|
||||
m_preloadImages->setValue(settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt());
|
||||
m_preloadImages->setToolTip(tr("How many images are preloaded before and after current image."));
|
||||
|
||||
m_thumSize = new EvenNumber(this);
|
||||
m_thumSize->setRange(64, 512);
|
||||
m_thumSize->setSingleStep(2);
|
||||
m_thumSize->setValue(settings.value("settings/thumnailsize", THUMB_SIZE).toInt());
|
||||
m_thumSize->setToolTip(tr("Thumbnail size in pixels"));
|
||||
|
||||
m_saturation = new QDoubleSpinBox(this);
|
||||
m_saturation->setMinimum(0);
|
||||
m_saturation->setMaximum(100);
|
||||
m_saturation->setSuffix(" %");
|
||||
m_saturation->setValue(settings.value("settings/saturation", SATURATION * 100.0).toDouble());
|
||||
m_saturation->setToolTip(tr("Set threshold value that is considered saturated when showing statistics.\nFor RAW files you may set 22%"));
|
||||
|
||||
m_useNativeDialog = new QCheckBox(tr("Don't use native file dialog"), this);
|
||||
m_useNativeDialog->setChecked(QApplication::testAttribute(Qt::AA_DontUseNativeDialogs));
|
||||
|
||||
layout->addRow(tr("Image preload count"), m_preloadImages);
|
||||
layout->addRow(tr("Thumbnails size"), m_thumSize);
|
||||
layout->addRow(tr("Saturation"), m_saturation);
|
||||
layout->addRow(m_useNativeDialog);
|
||||
//layout->addRow(new QLabel(tr("Changes in settings will take effect after program restart.")));
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
|
||||
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(this, &QDialog::accepted, this, &SettingsDialog::saveSettings);
|
||||
|
||||
layout->addRow(buttonBox);
|
||||
}
|
||||
|
||||
void SettingsDialog::loadSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
DEFAULT_WIDTH = settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt();
|
||||
SATURATION = settings.value("settings/saturation", 95.0).toDouble() / 100.0;
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, settings.value("settings/dontusenativedialogs", false).toBool());
|
||||
}
|
||||
|
||||
bool SettingsDialog::loadThumbsizes()
|
||||
{
|
||||
QSettings settings;
|
||||
int OLD_THUMB_SIZE = THUMB_SIZE;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
return OLD_THUMB_SIZE != THUMB_SIZE;
|
||||
}
|
||||
|
||||
void SettingsDialog::saveSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("settings/thumbnailsize", m_thumSize->value());
|
||||
settings.setValue("settings/preloadimagecount", m_preloadImages->value());
|
||||
settings.setValue("settings/dontusenativedialogs", m_useNativeDialog->isChecked());
|
||||
settings.setValue("settings/saturation", m_saturation->value());
|
||||
SATURATION = m_saturation->value() / 100.0;
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, m_useNativeDialog->isChecked());
|
||||
if(DEFAULT_WIDTH != m_preloadImages->value())
|
||||
emit preloadChanged(m_preloadImages->value());
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
uniform ivec2 firstRed;
|
||||
in vec2 qt_TexCoord0;
|
||||
in vec2 center;
|
||||
out vec4 color;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
#define f(x, y) texelFetch(qt_Texture0, icenter + ivec2(x, y), 0).r
|
||||
|
||||
@@ -11,7 +10,7 @@ void main(void)
|
||||
{
|
||||
ivec2 texSize = textureSize(qt_Texture0, 0);
|
||||
ivec2 icenter = ivec2(center);
|
||||
ivec2 alternate = icenter % 2;
|
||||
ivec2 alternate = (icenter + firstRed) % 2;
|
||||
|
||||
// cross, checker, theta, phi
|
||||
const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
uniform vec3 mtf_param;
|
||||
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 vec3 whiteBalance;
|
||||
uniform bool false_color;
|
||||
uniform int filtering;
|
||||
uniform int colormapIdx;
|
||||
in vec2 qt_TexCoord0;
|
||||
out vec4 color;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec3 Linear2sRGB(vec3 color)
|
||||
{
|
||||
@@ -16,11 +19,18 @@ vec3 Linear2sRGB(vec3 color)
|
||||
greaterThan(color, vec3(0.0031308)));
|
||||
}
|
||||
|
||||
vec4 MTF(vec4 x, vec3 m)
|
||||
vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
|
||||
{
|
||||
x = (x - m.x) / (m.z - m.x);
|
||||
x = (x - low) / (high - low);
|
||||
x = clamp(x, vec4(0.0), vec4(1.0));
|
||||
return ((m.y - 1) * x) / ((2 * m.y - 1) * x - m.y);
|
||||
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()
|
||||
@@ -29,19 +39,121 @@ vec3 checker()
|
||||
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)
|
||||
{
|
||||
color = texture(qt_Texture0, qt_TexCoord0);
|
||||
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, mtf_param);
|
||||
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 = Linear2sRGB(color.rgb);
|
||||
|
||||
color.rgb *= whiteBalance;
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
|
||||
@@ -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>
|
||||
@@ -1,22 +1,20 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2DArray qt_Texture0;
|
||||
uniform vec3 mtf_param;
|
||||
uniform vec3 mtf_param[3];
|
||||
uniform bool invert;
|
||||
in vec3 qt_TexCoord0;
|
||||
out vec4 color;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec4 MTF(vec4 x, vec3 m)
|
||||
vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
|
||||
{
|
||||
x = (x - m.x) / (m.z - m.x);
|
||||
x = (x - low) / (high - low);
|
||||
x = clamp(x, vec4(0.0), vec4(1.0));
|
||||
return ((m.y - 1) * x) / ((2 * m.y - 1) * x - m.y);
|
||||
return ((mid - 1.0) * x) / ((2.0 * mid - 1.0) * x - mid);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
color = texture(qt_Texture0, qt_TexCoord0);
|
||||
color = MTF(color, mtf_param);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#version 330
|
||||
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
in ivec3 imageSize_num;
|
||||
@@ -13,9 +11,9 @@ void main(void)
|
||||
{
|
||||
vec2 pos = qt_Vertex * 0.5;
|
||||
pos.y *= -1.0;
|
||||
pos = pos * imageSize_num.xy + thumb_size.x;
|
||||
pos = pos * vec2(imageSize_num.xy) + float(thumb_size.x);
|
||||
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * thumb_size.yz;
|
||||
|
||||
gl_Position = mvp * vec4(pos - offset + off, 0.0, 1.0);
|
||||
qt_TexCoord0 = vec3(qt_MultiTexCoord0, imageSize_num.z + 0.1);
|
||||
gl_Position = mvp * vec4(pos - offset + vec2(off), 0.0, 1.0);
|
||||
qt_TexCoord0 = vec3(qt_MultiTexCoord0, float(imageSize_num.z) + 0.1);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
<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 bit integer and 32 bit float</li>
|
||||
<li>XISF 8, 16 bit integer and 32 bit float</li>
|
||||
<li>RAW CR2, DNG, NEF</li>
|
||||
<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>
|
||||
@@ -27,6 +31,9 @@
|
||||
<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>
|
||||
@@ -42,12 +49,137 @@
|
||||
<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="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>
|
||||
|
||||
@@ -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>
|
||||
@@ -31,18 +31,23 @@ About::About(QWidget *parent) : QDialog(parent)
|
||||
HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Help"));
|
||||
resize(800, 600);
|
||||
|
||||
QLocale locale;
|
||||
QString l = QLocale::languageToString(locale.language());
|
||||
resize(1000, 600);
|
||||
setModal(false);
|
||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
QTextEdit *helpText = new QTextEdit(this);
|
||||
helpText->setReadOnly(true);
|
||||
layout->addWidget(helpText);
|
||||
|
||||
QFile tenmonText(":/help");
|
||||
tenmonText.open(QIODevice::ReadOnly);
|
||||
helpText->setHtml(tenmonText.readAll());
|
||||
|
||||
layout->addWidget(helpText);
|
||||
}
|
||||
|
||||
QString getVersion()
|
||||
{
|
||||
QString version = GITVERSION;
|
||||
version.truncate(8);
|
||||
return version;
|
||||
}
|
||||
@@ -17,4 +17,6 @@ public:
|
||||
HelpDialog(QWidget *parent = nullptr);
|
||||
};
|
||||
|
||||
QString getVersion();
|
||||
|
||||
#endif // ABOUT_H
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -4,43 +4,63 @@
|
||||
#include <QSqlError>
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include "loadrunable.h"
|
||||
#include "loadimage.h"
|
||||
|
||||
Database::Database(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool Database::init()
|
||||
bool Database::init(const QLatin1String &connectionName)
|
||||
{
|
||||
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir dir(path);
|
||||
|
||||
QSqlDatabase m_database = QSqlDatabase::addDatabase("QSQLITE");
|
||||
database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||
ngc = QSqlDatabase::addDatabase("QSQLITE", connectionName + "ngc");
|
||||
|
||||
if(!dir.mkpath("."))
|
||||
return false;
|
||||
|
||||
if(m_database.isValid())
|
||||
if(ngc.isValid())
|
||||
{
|
||||
m_database.setDatabaseName(dir.absoluteFilePath("database2.db"));
|
||||
if(m_database.open())
|
||||
QString ngcDb = dir.absoluteFilePath("ngc.db");
|
||||
if(!QFile::exists(ngcDb))
|
||||
QFile::copy(":/ngc.db", ngcDb);
|
||||
|
||||
ngc.setDatabaseName(ngcDb);
|
||||
if(ngc.open())
|
||||
{
|
||||
m_database.exec("PRAGMA foreign_keys = ON");
|
||||
int version = checkVersion();
|
||||
m_getNgc = QSqlQuery(ngc);
|
||||
m_getNgc.prepare("SELECT *,IIF(V_Mag IS NULL, B_Mag, V_Mag) AS mag FROM ngc WHERE RA_deg BETWEEN ? AND ? AND DEC_deg BETWEEN ? AND ?");
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
m_database.exec("PRAGMA user_version = 1");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
|
||||
query.exec("PRAGMA user_version = 1");
|
||||
query.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
|
||||
query.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
|
||||
" minRa REAL, maxRa REAL, minDec REAL, maxDec REAL, crVal1 REAL, crVal2 REAL)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
|
||||
query.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
|
||||
"key VARCHAR(81), value VARCHAR(81), comment VARCHAR(81), FOREIGN KEY(id_file) REFERENCES fits_files(id) ON DELETE CASCADE)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
|
||||
query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
|
||||
}
|
||||
else if(version > 1)
|
||||
{
|
||||
@@ -48,33 +68,41 @@ bool Database::init()
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlError error = m_database.lastError();
|
||||
QSqlError error = database.lastError();
|
||||
|
||||
if(error.type() == QSqlError::NoError)
|
||||
{
|
||||
m_markQuery = QSqlQuery(m_database);
|
||||
m_markQuery = QSqlQuery(database);
|
||||
m_markQuery.prepare("INSERT INTO files (file) VALUES (?)");
|
||||
m_unmarkQuery = QSqlQuery(m_database);
|
||||
m_unmarkQuery = QSqlQuery(database);
|
||||
m_unmarkQuery.prepare("DELETE FROM files WHERE file = (?)");
|
||||
m_isMarkedQuery = QSqlQuery(m_database);
|
||||
m_isMarkedQuery = QSqlQuery(database);
|
||||
m_isMarkedQuery.prepare("SELECT * FROM files WHERE file = (:name)");
|
||||
|
||||
m_insertFile = QSqlQuery(m_database);
|
||||
m_insertFile = QSqlQuery(database);
|
||||
m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
|
||||
m_insertFileWcs = QSqlQuery(m_database);
|
||||
m_insertFileWcs = QSqlQuery(database);
|
||||
m_insertFileWcs.prepare("INSERT INTO fits_files (file, mtime, minRa, maxRa, minDec, maxDec, crVal1, crVal2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
m_insertFitsHeader = QSqlQuery(m_database);
|
||||
m_insertFitsHeader = QSqlQuery(database);
|
||||
m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
|
||||
m_checkFile = QSqlQuery(m_database);
|
||||
m_checkFile = QSqlQuery(database);
|
||||
m_checkFile.prepare("SELECT id,mtime FROM fits_files WHERE file=?");
|
||||
m_headerKeywords = QSqlQuery(m_database);
|
||||
m_headerKeywords = QSqlQuery(database);
|
||||
m_headerKeywords.prepare("SELECT DISTINCT key FROM fits_headers ORDER BY key");
|
||||
m_deleteFile = QSqlQuery(m_database);
|
||||
m_deleteFile = QSqlQuery(database);
|
||||
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
|
||||
return true;
|
||||
}
|
||||
qDebug() << error.text();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to open database" << connectionName;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Database is invalid";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -130,7 +158,7 @@ QStringList Database::getMarkedFiles()
|
||||
|
||||
void Database::clearMarkedFiles()
|
||||
{
|
||||
QSqlDatabase::database().exec("DELETE FROM files");
|
||||
QSqlQuery query("DELETE FROM files");
|
||||
}
|
||||
|
||||
bool Database::checkError(QSqlQuery &query)
|
||||
@@ -145,34 +173,38 @@ bool Database::checkError(QSqlQuery &query)
|
||||
}
|
||||
}
|
||||
|
||||
int Database::checkVersion()
|
||||
int Database::checkVersion(QSqlDatabase &db)
|
||||
{
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
QSqlQuery query = db.exec("PRAGMA user_version");
|
||||
QSqlQuery query("PRAGMA user_version", db);
|
||||
if(query.next())
|
||||
return query.value(0).toInt();
|
||||
return -1;
|
||||
}
|
||||
|
||||
static QStringList nameFilters = {"*.fit", "*.fits", "*.xisf"};
|
||||
static QStringList nameFilters = {"*.fit", "*.fits", "*.fz", "*.fts", "*.xisf"};
|
||||
|
||||
static int countFiles(const QDir &dir, int count = 0)
|
||||
static int countFiles(const QDir &dir, QStringList &scannedDirs)
|
||||
{
|
||||
count += dir.entryList(nameFilters, QDir::Files).size();
|
||||
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));
|
||||
count += countFiles(dir.filePath(d), scannedDirs);
|
||||
return count;
|
||||
}
|
||||
|
||||
void Database::indexDir(const QDir &dir, QProgressDialog *progress)
|
||||
{
|
||||
m_progress = 0;
|
||||
int count = countFiles(dir);
|
||||
QStringList scannedDirs;
|
||||
int count = countFiles(dir, scannedDirs);
|
||||
progress->setMaximum(count);
|
||||
QSqlDatabase database = QSqlDatabase::database();
|
||||
database.transaction();
|
||||
if(indexDir2(dir, progress))
|
||||
|
||||
scannedDirs.clear();
|
||||
if(indexDir2(dir, progress, scannedDirs))
|
||||
{
|
||||
database.commit();
|
||||
emit databaseChanged();
|
||||
@@ -186,12 +218,11 @@ void Database::indexDir(const QDir &dir, QProgressDialog *progress)
|
||||
void Database::reindex(QProgressDialog *progress)
|
||||
{
|
||||
QVariantList deleteids;
|
||||
QSqlDatabase database = QSqlDatabase::database();
|
||||
database.transaction();
|
||||
QSqlQuery size = database.exec("SELECT COUNT(*) FROM fits_files");
|
||||
QSqlQuery size("SELECT COUNT(*) FROM fits_files", database);
|
||||
size.next();
|
||||
progress->setMaximum(size.value(0).toInt());
|
||||
QSqlQuery files = database.exec("SELECT id,file,mtime FROM fits_files");
|
||||
QSqlQuery files("SELECT id,file,mtime FROM fits_files", database);
|
||||
int i = 0;
|
||||
while(files.next())
|
||||
{
|
||||
@@ -225,14 +256,68 @@ QStringList Database::getFitsKeywords()
|
||||
return keywords;
|
||||
}
|
||||
|
||||
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
|
||||
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))
|
||||
if(!indexDir2(dir.filePath(d), progress, scannedDirs))
|
||||
return false;
|
||||
}
|
||||
for(const QFileInfo &file : files)
|
||||
@@ -262,10 +347,10 @@ bool Database::indexFile(const QFileInfo &file)
|
||||
}
|
||||
}
|
||||
|
||||
bool ok;
|
||||
if(filePath.endsWith(".xisf", Qt::CaseInsensitive))
|
||||
bool ok = false;
|
||||
if(isXISF(file.suffix()))
|
||||
ok = readXISFHeader(filePath, info);
|
||||
else
|
||||
else if(isFITS(file.suffix()))
|
||||
ok = readFITSHeader(filePath, info);
|
||||
|
||||
qlonglong last_id = -1;
|
||||
@@ -306,6 +391,7 @@ bool Database::indexFile(const QFileInfo &file)
|
||||
QVariantList file_id, keys, values, comments;
|
||||
for(const auto &record : info.fitsHeader)
|
||||
{
|
||||
if(record.xisf && record.key.startsWith("PCL:"))continue;
|
||||
file_id << last_id;
|
||||
keys << QString(record.key);
|
||||
values << record.value.toString();
|
||||
@@ -6,10 +6,13 @@
|
||||
#include <QSqlQuery>
|
||||
#include <QDir>
|
||||
#include <QProgressDialog>
|
||||
#include "imageinfodata.h"
|
||||
|
||||
class Database : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QSqlDatabase database;
|
||||
QSqlDatabase ngc;
|
||||
QSqlQuery m_markQuery;
|
||||
QSqlQuery m_unmarkQuery;
|
||||
QSqlQuery m_isMarkedQuery;
|
||||
@@ -21,10 +24,12 @@ class Database : public QObject
|
||||
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);
|
||||
@@ -36,11 +41,12 @@ public:
|
||||
void indexDir(const QDir &dir, QProgressDialog *progress);
|
||||
void reindex(QProgressDialog *progress);
|
||||
QStringList getFitsKeywords();
|
||||
QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec);
|
||||
protected:
|
||||
bool indexDir2(const QDir &dir, QProgressDialog *progress);
|
||||
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
|
||||
bool indexFile(const QFileInfo &file);
|
||||
bool checkError(QSqlQuery &query);
|
||||
int checkVersion();
|
||||
int checkVersion(QSqlDatabase &db);
|
||||
signals:
|
||||
void databaseChanged();
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QContextMenuEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <iostream>
|
||||
#include "batchprocessing.h"
|
||||
|
||||
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
|
||||
|
||||
@@ -123,6 +124,10 @@ QVariant FITSFileModel::data(const QModelIndex &index, int role) const
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -197,7 +202,8 @@ void FITSFileModel::prepareQuery()
|
||||
if(lastError().type() != QSqlError::NoError)
|
||||
qDebug() << "Database error" << lastError();
|
||||
|
||||
m_markedFiles = m_database->getMarkedFiles().toSet();
|
||||
QStringList list = m_database->getMarkedFiles();
|
||||
m_markedFiles = QSet<QString>(list.begin(), list.end());
|
||||
}
|
||||
|
||||
DatabaseTableView::DatabaseTableView(QWidget *parent) : QTableView(parent)
|
||||
@@ -209,6 +215,8 @@ 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)
|
||||
@@ -220,7 +228,10 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
|
||||
emit filesMarked(indexes);
|
||||
else if(a == unmark)
|
||||
emit filesUnmarked(indexes);
|
||||
|
||||
else if(a == open)
|
||||
emit openFile(indexes);
|
||||
else if(a == openDirAction)
|
||||
emit openDir(indexes);
|
||||
}
|
||||
|
||||
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
|
||||
@@ -265,6 +276,17 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
m_database->unmark(files);
|
||||
m_model->filesUnmarked(indexes);
|
||||
});
|
||||
connect(m_tableView, &DatabaseTableView::openFile, [this](QModelIndexList indexes){
|
||||
if(indexes.size())
|
||||
emit loadFile(m_model->data(indexes.front().siblingAtColumn(0)).toString());
|
||||
});
|
||||
connect(m_tableView, &DatabaseTableView::openDir, [this](QModelIndexList indexes){
|
||||
if(indexes.size())
|
||||
{
|
||||
QFileInfo info(m_model->data(indexes.front().siblingAtColumn(0)).toString());
|
||||
openDir(info.absolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
auto addFilterItems = [](QComboBox *combobox, const QStringList &fitsKeywords)
|
||||
{
|
||||
@@ -281,6 +303,7 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
for(int i=0; i<3; i++)
|
||||
{
|
||||
m_filterKeyword[i] = new QComboBox(this);
|
||||
m_filterKeyword[i]->setMaximumWidth(300);
|
||||
addFilterItems(m_filterKeyword[i], fitsKeywords);
|
||||
|
||||
|
||||
@@ -302,7 +325,7 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
}
|
||||
|
||||
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
|
||||
connect(filterButton, SIGNAL(pressed()), this, SLOT(applyFilter()));
|
||||
connect(filterButton, &QPushButton::pressed, this, &DataBaseView::applyFilter);
|
||||
hlayout->addWidget(filterButton);
|
||||
|
||||
connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
|
||||
@@ -369,7 +392,7 @@ bool DataBaseView::exportCSV(const QString &path)
|
||||
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return false;
|
||||
|
||||
QSqlQuery sql = m_model->query();
|
||||
QSqlQuery sql(m_model->query().lastQuery());
|
||||
int colCount = m_model->columnCount();
|
||||
QStringList header;
|
||||
for(int i=0; i<colCount; i++)
|
||||
@@ -52,6 +52,8 @@ protected:
|
||||
signals:
|
||||
void filesMarked(QModelIndexList indexes);
|
||||
void filesUnmarked(QModelIndexList indexes);
|
||||
void openFile(QModelIndexList indexes);
|
||||
void openDir(QModelIndexList indexes);
|
||||
};
|
||||
|
||||
class DataBaseView : public QWidget
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FileManager</class>
|
||||
<widget class="QMainWindow" name="FileManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1282</width>
|
||||
<height>858</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>File Manager</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="leftLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="leftPath"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DirView" name="leftTab"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="rightLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="rightPath"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DirView" name="rightTab"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="progressLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1282</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuLeft_Tab">
|
||||
<property name="title">
|
||||
<string>Left Tab</string>
|
||||
</property>
|
||||
<addaction name="actionLoad_FITS_keywordsLeft"/>
|
||||
<addaction name="actionSelect_columnsLeft"/>
|
||||
<addaction name="actionCopySelectedFilesPathsLeft"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuRight_Tab">
|
||||
<property name="title">
|
||||
<string>Right Tab</string>
|
||||
</property>
|
||||
<addaction name="actionLoad_FITS_keywordsRight"/>
|
||||
<addaction name="actionSelect_columnsRight"/>
|
||||
<addaction name="actionCopySelectedFilesPathsRight"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<addaction name="menuLeft_Tab"/>
|
||||
<addaction name="menuRight_Tab"/>
|
||||
</widget>
|
||||
<action name="actionLoad_FITS_keywordsLeft">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Load FITS keywords</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoad_FITS_keywordsRight">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Load FITS keywords</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSelect_columnsLeft">
|
||||
<property name="text">
|
||||
<string>Select columns</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSelect_columnsRight">
|
||||
<property name="text">
|
||||
<string>Select columns</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCopySelectedFilesPathsLeft">
|
||||
<property name="text">
|
||||
<string>Copy selected files paths</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCopySelectedFilesPathsRight">
|
||||
<property name="text">
|
||||
<string>Copy selected files paths</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DirView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>filemanager.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
#include <QHeaderView>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) : QWidget(parent)
|
||||
, m_model(model)
|
||||
@@ -20,6 +21,18 @@ FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) :
|
||||
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);
|
||||
@@ -33,13 +46,19 @@ void FilesystemWidget::fileClicked(const QModelIndex &index, const QModelIndex &
|
||||
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 QFileSystemModel(this);
|
||||
m_fileSystemModel = new FileSystemModel(this);
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
|
||||
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.fz", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
|
||||
m_fileSystemModel->setNameFilterDisables(false);
|
||||
if(settings.value("filetree/showHidden", false).toBool())
|
||||
m_fileSystemModel->setFilter(m_fileSystemModel->filter() | QDir::Hidden);
|
||||
@@ -99,6 +118,7 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
setRootIndex(index);
|
||||
m_rootDir = m_fileSystemModel->filePath(index);
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
}
|
||||
else if(a == resetRoot)
|
||||
{
|
||||
@@ -109,6 +129,7 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
setRootIndex(rootIndex().parent());
|
||||
m_rootDir = m_fileSystemModel->filePath(rootIndex().parent());
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
}
|
||||
else if(a == copy)
|
||||
{
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QWidget>
|
||||
#include <QFileSystemModel>
|
||||
#include <QIdentityProxyModel>
|
||||
#include <QListView>
|
||||
#include <QTreeView>
|
||||
|
||||
@@ -13,17 +14,28 @@ class FilesystemWidget : public QWidget
|
||||
QAbstractItemModel *m_model;
|
||||
public:
|
||||
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
|
||||
private slots:
|
||||
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
|
||||
QFileSystemModel *m_fileSystemModel;
|
||||
FileSystemModel *m_fileSystemModel;
|
||||
QString m_rootDir;
|
||||
public:
|
||||
explicit Filetree(QWidget *parent = nullptr);
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 1400–2000
|
||||
// index-4118.fits 1000–1400
|
||||
// index-4117.fits 680–1000
|
||||
// index-4116.fits 480–680
|
||||
// index-4115.fits 340–480
|
||||
// index-4114.fits 240–340
|
||||
// index-4113.fits 170–240
|
||||
// index-4112.fits 120–170
|
||||
// index-4111.fits 85–120
|
||||
// index-4110.fits 60—85
|
||||
// index-4109.fits 42–60
|
||||
// index-4108.fits 30–42
|
||||
// index-4107.fits 22–30
|
||||
// index-5206-*.fits 16–22
|
||||
// index-5205-*.fits 11–16
|
||||
// index-5204-*.fits 8–11
|
||||
// index-5203-*.fits 5.6–8.0
|
||||
// index-5202-*.fits 4.0–5.6
|
||||
// index-5201-*.fits 2.8–4.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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "imageinfo.h"
|
||||
#include <QSettings>
|
||||
#include <QHeaderView>
|
||||
|
||||
QMap<QString, QColor> headerHighlight;
|
||||
|
||||
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
|
||||
{
|
||||
setColumnCount(3);
|
||||
setHeaderLabels({tr("Property"), tr("Value"), tr("Comment")});
|
||||
setIndentation(5);
|
||||
QSettings settings;
|
||||
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
|
||||
}
|
||||
|
||||
ImageInfo::~ImageInfo()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("imageinfo/headerstate", header()->saveState());
|
||||
}
|
||||
|
||||
void ImageInfo::setInfo(const ImageInfoData &info)
|
||||
{
|
||||
clear();
|
||||
if(info.fitsHeader.size())
|
||||
{
|
||||
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
|
||||
for(const FITSRecord &record : info.fitsHeader)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
|
||||
if(headerHighlight.contains(record.key))
|
||||
{
|
||||
QColor color = headerHighlight[record.key];
|
||||
item->setBackground(0, color);
|
||||
item->setBackground(1, color);
|
||||
item->setBackground(2, color);
|
||||
}
|
||||
}
|
||||
addTopLevelItem(fitsHeader);
|
||||
}
|
||||
if(info.info.size())
|
||||
{
|
||||
QTreeWidgetItem *infoHeader = new QTreeWidgetItem({tr("Image info")});
|
||||
for(auto &item : info.info)
|
||||
{
|
||||
new QTreeWidgetItem(infoHeader, {item.first, item.second});
|
||||
}
|
||||
addTopLevelItem(infoHeader);
|
||||
}
|
||||
expandAll();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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, °) * 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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "imageringlist.h"
|
||||
#include <functional>
|
||||
#include <QThreadPool>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QRegularExpression>
|
||||
#include "loadrunable.h"
|
||||
#include "rawimage.h"
|
||||
#include "database.h"
|
||||
@@ -19,13 +23,16 @@ Image::Image(const QString name, int number, ImageRingList *ringList) :
|
||||
{
|
||||
}
|
||||
|
||||
void Image::load()
|
||||
void Image::load(int index, QThreadPool *pool)
|
||||
{
|
||||
if(index != m_info.index && !m_loading)
|
||||
m_rawImage.reset();
|
||||
|
||||
if(!m_rawImage && !m_loading)
|
||||
{
|
||||
m_loading = true;
|
||||
m_released = false;
|
||||
QThreadPool::globalInstance()->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel()));
|
||||
pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel(), index));
|
||||
}
|
||||
if(!m_loading && m_rawImage)
|
||||
emit pixmapLoaded(this);
|
||||
@@ -34,7 +41,7 @@ void Image::load()
|
||||
void Image::loadThumbnail(QThreadPool *pool)
|
||||
{
|
||||
if(!m_thumbnail)
|
||||
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, true));
|
||||
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, 0, true));
|
||||
else
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
@@ -81,24 +88,25 @@ void Image::clearThumbnail()
|
||||
m_thumbnail.reset();
|
||||
}
|
||||
|
||||
void Image::imageLoaded(void *rawImage, ImageInfoData info)
|
||||
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.reset(static_cast<RawImage*>(rawImage));
|
||||
m_rawImage = rawImage;
|
||||
m_info = info;
|
||||
emit pixmapLoaded(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
delete static_cast<RawImage*>(rawImage);
|
||||
}
|
||||
}
|
||||
|
||||
void Image::thumbnailLoadFinish(void *rawImage)
|
||||
void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
|
||||
{
|
||||
m_thumbnail.reset(static_cast<RawImage*>(rawImage));
|
||||
m_thumbnail = rawImage;
|
||||
if(m_thumbnail)
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
@@ -108,36 +116,70 @@ ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter,
|
||||
, m_analyzeLevel(None)
|
||||
, m_database(database)
|
||||
, m_nameFilter(nameFilter)
|
||||
, m_fileSuffix(nameFilter)
|
||||
{
|
||||
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
|
||||
connect(&m_fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &ImageRingList::dirChanged);
|
||||
m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
|
||||
m_loadPool = new QThreadPool(this);
|
||||
m_loadPool->setThreadPriority(QThread::LowPriority);
|
||||
m_thumbPool = new QThreadPool(this);
|
||||
m_thumbPool->setThreadPriority(QThread::LowPriority);
|
||||
|
||||
m_slideShowTimer = new QTimer(this);
|
||||
connect(m_slideShowTimer, &QTimer::timeout, this, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
|
||||
|
||||
m_dirChangeDelay = new QTimer(this);
|
||||
m_dirChangeDelay->setInterval(3000);
|
||||
m_dirChangeDelay->setSingleShot(true);
|
||||
connect(m_dirChangeDelay, &QTimer::timeout, this, &ImageRingList::reloadDir);
|
||||
}
|
||||
|
||||
ImageRingList::~ImageRingList()
|
||||
{
|
||||
QThreadPool::globalInstance()->clear();
|
||||
m_loadPool->clear();
|
||||
m_thumbPool->clear();
|
||||
|
||||
QThreadPool::globalInstance()->waitForDone();
|
||||
m_loadPool->waitForDone();
|
||||
m_thumbPool->waitForDone();
|
||||
}
|
||||
|
||||
bool ImageRingList::setDir(const QString path, const QString ¤tFile)
|
||||
bool ImageRingList::setDir(const QString path, const QString ¤tFile, bool recursive)
|
||||
{
|
||||
QDir dir(path);
|
||||
|
||||
if(dir.exists())
|
||||
{
|
||||
QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name | QDir::IgnoreCase);
|
||||
m_currentDir = path;
|
||||
QStringList scannedDirs;
|
||||
QStringList absolutePaths;
|
||||
foreach(const QString &file, list)
|
||||
std::function<void(const QString&)> scanDir = [&](const QString &path)
|
||||
{
|
||||
absolutePaths.append(dir.absoluteFilePath(file));
|
||||
}
|
||||
setFiles(absolutePaths, m_liveMode ? list.first() : currentFile);
|
||||
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;
|
||||
|
||||
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
|
||||
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;
|
||||
}
|
||||
@@ -146,11 +188,25 @@ bool ImageRingList::setDir(const QString path, const QString ¤tFile)
|
||||
|
||||
void ImageRingList::setFile(const QString &file)
|
||||
{
|
||||
QFileInfo info(file);
|
||||
if(info.isDir())
|
||||
setDir(file);
|
||||
else
|
||||
setDir(info.absolutePath(), 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([®](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()
|
||||
@@ -161,16 +217,25 @@ ImagePtr ImageRingList::currentImage()
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString ImageRingList::currentDir() const
|
||||
{
|
||||
return m_currentDir;
|
||||
}
|
||||
|
||||
void ImageRingList::increment()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
//don't increment if current image was not loaded yet
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
(*m_firstImage)->release();
|
||||
m_firstImage = increment(m_firstImage);
|
||||
m_currImage = increment(m_currImage);
|
||||
(*m_currImage)->load();
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +243,64 @@ void ImageRingList::decrement()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
//don't decrement if current image was not loaded yet
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
(*m_lastImage)->release();
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
m_currImage = decrement(m_currImage);
|
||||
(*m_currImage)->load();
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
m_lastImage = decrement(m_lastImage);
|
||||
(*m_firstImage)->load();
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::prevSubImage()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
int index = (*m_currImage)->info().index;
|
||||
int num = (*m_currImage)->info().num;
|
||||
if(num > 1)
|
||||
(*m_currImage)->load(index == 0 ? num - 1 : index - 1, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::nextSubImage()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
int index = (*m_currImage)->info().index;
|
||||
int num = (*m_currImage)->info().num;
|
||||
if(num > 1)
|
||||
(*m_currImage)->load((index + 1) % num, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::setMarked()
|
||||
{
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
files.removeIf([](const QString &file){
|
||||
QFileInfo info(file);
|
||||
return !info.exists() || !info.isReadable();
|
||||
});
|
||||
setFilesPrivate(files);
|
||||
}
|
||||
|
||||
void ImageRingList::reloadImage()
|
||||
{
|
||||
if(*m_currImage)
|
||||
{
|
||||
int index = (*m_currImage)->info().index;
|
||||
(*m_currImage)->release();
|
||||
(*m_currImage)->load(index, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +344,7 @@ void ImageRingList::loadFile(int row)
|
||||
if(m_images.empty())
|
||||
return;
|
||||
|
||||
(*m_currImage)->load();
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
|
||||
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(m_liveMode)
|
||||
@@ -236,9 +353,9 @@ void ImageRingList::loadFile(int row)
|
||||
for(int i=0; i<m_width; i++)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
if(m_lastImage != m_firstImage)
|
||||
{
|
||||
@@ -297,11 +414,13 @@ void ImageRingList::clearThumbnails()
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -315,6 +434,7 @@ int ImageRingList::rowCount(const QModelIndex &parent) const
|
||||
|
||||
int ImageRingList::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -359,9 +479,9 @@ void ImageRingList::setPreload(int width)
|
||||
for(int i = newWidth - m_width; i>0; i--)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
if(newWidth < m_width)
|
||||
@@ -377,11 +497,48 @@ void ImageRingList::setPreload(int width)
|
||||
m_width = newWidth;
|
||||
}
|
||||
|
||||
void ImageRingList::setFiles(const QStringList files, const QString ¤tFile)
|
||||
void ImageRingList::setSort(QDir::SortFlag sort)
|
||||
{
|
||||
QThreadPool::globalInstance()->clear();
|
||||
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 ¤tFile)
|
||||
{
|
||||
m_loadPool->clear();
|
||||
m_thumbPool->clear();
|
||||
QThreadPool::globalInstance()->waitForDone();
|
||||
m_loadPool->waitForDone();
|
||||
m_thumbPool->waitForDone();
|
||||
beginResetModel();
|
||||
m_images.clear();
|
||||
@@ -389,8 +546,8 @@ void ImageRingList::setFiles(const QStringList files, const QString ¤tFile
|
||||
for(const QString &file : files)
|
||||
{
|
||||
ImagePtr ptr = make_shared<Image>(file, i++, this);
|
||||
connect(ptr.get(), SIGNAL(pixmapLoaded(Image*)), this, SLOT(imageLoaded(Image*)));
|
||||
connect(ptr.get(), SIGNAL(thumbnailLoaded(Image*)), this, SIGNAL(thumbnailLoaded(Image*)));
|
||||
connect(ptr.get(), &Image::pixmapLoaded, this, &ImageRingList::imageLoaded);
|
||||
connect(ptr.get(), &Image::thumbnailLoaded, this, &ImageRingList::thumbnailLoaded);
|
||||
m_images.append(ptr);
|
||||
}
|
||||
|
||||
@@ -429,14 +586,22 @@ void ImageRingList::imageLoaded(Image *image)
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::dirChanged(QString dir)
|
||||
void ImageRingList::dirChanged(QString)
|
||||
{
|
||||
if(m_liveMode)
|
||||
reloadDir();
|
||||
else
|
||||
m_dirChangeDelay->start();
|
||||
}
|
||||
|
||||
void ImageRingList::reloadDir()
|
||||
{
|
||||
QString currentFile;
|
||||
|
||||
if(m_images.size())
|
||||
currentFile = (*m_currImage)->name();
|
||||
|
||||
setDir(dir, currentFile);
|
||||
setDir(m_currentDir, currentFile);
|
||||
if(m_images.size())
|
||||
emit currentImageChanged(m_currImage-m_images.begin());
|
||||
}
|
||||
@@ -5,9 +5,11 @@
|
||||
#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;
|
||||
@@ -20,13 +22,13 @@ class Image : public QObject
|
||||
bool m_current;
|
||||
int m_number;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::unique_ptr<RawImage> m_thumbnail;
|
||||
std::shared_ptr<RawImage> m_thumbnail;
|
||||
QString m_name;
|
||||
ImageInfoData m_info;
|
||||
ImageRingList *m_ringList;
|
||||
public:
|
||||
explicit Image(const QString name, int number, ImageRingList *ringList);
|
||||
void load();
|
||||
void load(int index, QThreadPool *pool);
|
||||
void loadThumbnail(QThreadPool *pool);
|
||||
void release();
|
||||
QString name() const;
|
||||
@@ -36,12 +38,13 @@ public:
|
||||
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 thumbnailLoadFinish(void *rawImage);
|
||||
void imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info);
|
||||
void thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage);
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<Image> ImagePtr;
|
||||
@@ -58,18 +61,25 @@ 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(Database *database, const QStringList &nameFilter, QObject *parent = 0);
|
||||
~ImageRingList() override;
|
||||
bool setDir(const QString path, const QString ¤tFile = QString());
|
||||
bool setDir(const QString path, const QString ¤tFile = 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);
|
||||
@@ -91,8 +101,17 @@ public:
|
||||
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 ¤tFile = QString());
|
||||
void setFilesPrivate(const QStringList files, const QString ¤tFile = QString());
|
||||
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
|
||||
QList<ImagePtr>::iterator decrement(QList<ImagePtr>::iterator iter);
|
||||
signals:
|
||||
@@ -102,7 +121,8 @@ signals:
|
||||
void currentImageChanged(int index);
|
||||
protected slots:
|
||||
void imageLoaded(Image *image);
|
||||
void dirChanged(QString dir);
|
||||
void dirChanged(QString);
|
||||
void reloadDir();
|
||||
};
|
||||
|
||||
#endif // IMAGERINGLIST_H
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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());
|
||||
}
|
||||
@@ -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 ¶ms);
|
||||
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
|
||||
@@ -0,0 +1,143 @@
|
||||
#ifndef IMAGEWIDGET_H
|
||||
#define IMAGEWIDGET_H
|
||||
|
||||
#include <memory>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLTexture>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QPainterPath>
|
||||
#include "database.h"
|
||||
#include "rawimage.h"
|
||||
#include "imageinfodata.h"
|
||||
#include "stretchtoolbar.h"
|
||||
|
||||
class ImageWidget
|
||||
{
|
||||
public:
|
||||
ImageWidget(){}
|
||||
virtual ~ImageWidget(){}
|
||||
|
||||
virtual void setImage(std::shared_ptr<RawImage> image, int index) = 0;
|
||||
virtual void setWCS(std::shared_ptr<WCSDataT> wcs) = 0;
|
||||
|
||||
virtual void zoom(int zoom, const QPointF &mousePos = QPointF()) = 0;
|
||||
virtual void bestFit() = 0;
|
||||
|
||||
virtual void setBayerMask(int mask) = 0;
|
||||
virtual void setColormap(int colormap) = 0;
|
||||
virtual void setOffset(float dx, float dy) = 0;
|
||||
virtual void allocateThumbnails(const QStringList &paths) = 0;
|
||||
|
||||
virtual void setMTFParams(const MTFParam ¶ms) = 0;
|
||||
virtual void superPixel(bool enable) = 0;
|
||||
virtual void invert(bool enable) = 0;
|
||||
virtual void falseColor(bool enable) = 0;
|
||||
virtual QImage renderToImage() = 0;
|
||||
virtual void thumbnailLoaded(const Image *image) = 0;
|
||||
virtual void showThumbnail(bool enable) = 0;
|
||||
virtual void drawGrid(bool enable) = 0;
|
||||
|
||||
static QImage loadColormap();
|
||||
};
|
||||
|
||||
struct ImageThumb
|
||||
{
|
||||
QString name;
|
||||
QString path;
|
||||
QSize size;
|
||||
bool selected;
|
||||
bool dirty;
|
||||
};
|
||||
|
||||
class ImageWidgetGL : public QOpenGLWidget, public ImageWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
QOpenGLFunctions *f = nullptr;
|
||||
QOpenGLExtraFunctions *fx = nullptr;
|
||||
QTimer *m_updateTimer = nullptr;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_program;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_thumbnailProgram;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_debayerProgram;
|
||||
std::unique_ptr<QOpenGLBuffer> m_buffer;
|
||||
std::unique_ptr<QOpenGLBuffer> m_bufferSizes;
|
||||
std::unique_ptr<QOpenGLTexture> m_image;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vao;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vaoThumb;
|
||||
std::unique_ptr<QOpenGLTexture> m_thumbnailTexture;
|
||||
std::unique_ptr<QOpenGLTexture> m_lut;
|
||||
std::unique_ptr<QOpenGLTexture> m_colormap;
|
||||
GLuint m_debayerTex = 0;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::shared_ptr<WCSDataT> m_wcs;
|
||||
SkyGrid m_grid;
|
||||
int m_width, m_height;
|
||||
int m_imgWidth = -1, m_imgHeight = -1;
|
||||
int m_currentImg = 0;
|
||||
MTFParam m_mtfParams;
|
||||
float m_unit_scale[2] = {1.0f, 0.0f}; // scale and offset
|
||||
float m_dx = 0, m_dy = 0;
|
||||
float m_scale = 1.0f;
|
||||
int m_scaleStop = 0;
|
||||
bool m_bestFit = false;
|
||||
bool m_bwImg = false;
|
||||
bool m_falseColor = false;
|
||||
bool m_invert = false;
|
||||
bool m_superpixel = false;
|
||||
bool m_showThumbnails = false;
|
||||
bool m_selecting = false;
|
||||
bool m_sizesDirty = false;
|
||||
bool m_srgb = false;
|
||||
bool m_drawGrid = false;
|
||||
int m_thumbnailCount = 0;
|
||||
int m_maxTextureSize = 0;
|
||||
int m_maxArrayLayers = 0;
|
||||
int m_firstRed[2] = {0, 0};
|
||||
int m_colormapIdx = 0;
|
||||
QVector<ImageThumb> m_thumnails;
|
||||
Database *m_database = nullptr;
|
||||
QPointF m_lastPos;
|
||||
QString m_error;
|
||||
bool m_swPaint = false;
|
||||
public:
|
||||
explicit ImageWidgetGL(Database *database, QWidget *parent = nullptr);
|
||||
~ImageWidgetGL() override;
|
||||
void setImage(std::shared_ptr<RawImage> image, int index) override;
|
||||
void setWCS(std::shared_ptr<WCSDataT> wcs) override;
|
||||
void zoom(int zoom, const QPointF &mousePos = QPointF()) override;
|
||||
void bestFit() override;
|
||||
void allocateThumbnails(const QStringList &paths) override;
|
||||
QVector2D getImagePixelCoord(const QVector2D &pos);
|
||||
void setBayerMask(int mask) override;
|
||||
void setColormap(int colormap) override;
|
||||
void setOffset(float dx, float dy) override;
|
||||
void setMTFParams(const MTFParam ¶ms) override;
|
||||
void superPixel(bool enable) override;
|
||||
void invert(bool enable) override;
|
||||
void falseColor(bool enable) override;
|
||||
QImage renderToImage() override;
|
||||
void thumbnailLoaded(const Image *image) override;
|
||||
void showThumbnail(bool enable) override;
|
||||
void drawGrid(bool enable) override;
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void initializeGL() override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void thumbSelect(QMouseEvent *event);
|
||||
void debayer();
|
||||
void updateScrollBars();
|
||||
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 // IMAGEWIDGET_H
|
||||
@@ -0,0 +1,498 @@
|
||||
#include "loadimage.h"
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <libraw/libraw.h>
|
||||
#include <fitsio2.h>
|
||||
#include "libxisf.h"
|
||||
#include <libexif/exif-data.h>
|
||||
#include "rawimage.h"
|
||||
|
||||
QString makeUNCPath(const QString &path)
|
||||
{
|
||||
#ifdef Q_OS_WIN64
|
||||
if(!path.startsWith("\\\\") && !path.startsWith("//"))
|
||||
{
|
||||
QString tmp;
|
||||
QFileInfo info(path);
|
||||
tmp = info.absoluteFilePath();
|
||||
tmp = QDir::toNativeSeparators(tmp);
|
||||
tmp.prepend("\\\\?\\");
|
||||
qDebug() << "makeMaxPath" << path << tmp;
|
||||
return tmp;
|
||||
}
|
||||
#endif
|
||||
return path;
|
||||
}
|
||||
|
||||
int loadFITSHeader(fitsfile *file, ImageInfoData &info)
|
||||
{
|
||||
int imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
int nexist;
|
||||
int status = 0;
|
||||
char key[FLEN_KEYWORD];
|
||||
char val[FLEN_VALUE];
|
||||
char comm[FLEN_COMMENT];
|
||||
char strval[FLEN_VALUE];
|
||||
QVariant var;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
||||
fits_get_hdrspace(file, &nexist, nullptr, &status);
|
||||
for(int i=1; i<=nexist; i++)
|
||||
{
|
||||
fits_read_keyn(file, i, key, val, comm, &status);
|
||||
fits_read_key(file, TSTRING, key, strval, nullptr, &status);
|
||||
if(status == 0 || status == VALUE_UNDEFINED)
|
||||
{
|
||||
QString string(strval);
|
||||
bool isint;
|
||||
bool isdouble;
|
||||
double vald = string.toDouble(&isdouble);
|
||||
long long vall = string.toLongLong(&isint);
|
||||
if(isint)
|
||||
var = vall;
|
||||
else if(isdouble)
|
||||
var = vald;
|
||||
else if(status == VALUE_UNDEFINED)
|
||||
var = QVariant();
|
||||
else if(string == "T" || string == "F")
|
||||
var = string == "T";
|
||||
else
|
||||
var = string;
|
||||
status = 0;
|
||||
info.fitsHeader.append(FITSRecord(key, var, comm));
|
||||
}
|
||||
else
|
||||
{
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
char *header = nullptr;
|
||||
int nrec = 0;
|
||||
const char *exclist[] = {"PV1_1", "PV1_2"};
|
||||
fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
|
||||
if(status == 0)
|
||||
{
|
||||
info.wcs = std::make_shared<WCSDataT>(naxes[0], naxes[1], header, nrec);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
fits_free_memory(header, &status);
|
||||
return status;
|
||||
}
|
||||
|
||||
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
|
||||
{
|
||||
fitsfile *file;
|
||||
int status = 0;
|
||||
int num = 0;
|
||||
long naxes[3] = {0};
|
||||
|
||||
auto checkError = [&info, &status]()
|
||||
{
|
||||
char err[100];
|
||||
fits_get_errstatus(status, err);
|
||||
info.info.append({QObject::tr("Error"), QString(err)});
|
||||
qDebug() << "Failed to load FITS file" << err;
|
||||
return false;
|
||||
};
|
||||
|
||||
fits_open_diskfile(&file, path.toLocal8Bit().data(), READONLY, &status);
|
||||
if(status)return checkError();
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
if(status)return checkError();
|
||||
|
||||
int hdutype;
|
||||
int imgtype;
|
||||
int naxis;
|
||||
std::vector<int> imageIdxs;
|
||||
for(int i = 1; i <= num; i++)
|
||||
{
|
||||
fits_movabs_hdu(file, i, &hdutype, &status);if(status)return checkError();
|
||||
if(hdutype == IMAGE_HDU)
|
||||
{
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
|
||||
}
|
||||
}
|
||||
info.num = imageIdxs.size();
|
||||
info.index = index;
|
||||
|
||||
if(index >= imageIdxs.size())return false;
|
||||
|
||||
fits_movabs_hdu(file, imageIdxs[index], &hdutype, &status);if(status)return checkError();
|
||||
if(hdutype == IMAGE_HDU)
|
||||
{
|
||||
naxes[0] = naxes[1] = naxes[2] = 0;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();
|
||||
|
||||
if(hdutype == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
{
|
||||
RawImage::DataType type;
|
||||
int fitstype;
|
||||
long fpixel[3] = {1,1,1};
|
||||
switch(imgtype)
|
||||
{
|
||||
case BYTE_IMG:
|
||||
type = RawImage::UINT8;
|
||||
fitstype = TBYTE;
|
||||
break;
|
||||
case SHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TSHORT;
|
||||
break;
|
||||
case USHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case LONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TINT;
|
||||
break;
|
||||
case ULONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TUINT;
|
||||
break;
|
||||
case FLOAT_IMG:
|
||||
type = RawImage::FLOAT32;
|
||||
fitstype = TFLOAT;
|
||||
break;
|
||||
case DOUBLE_IMG:
|
||||
type = RawImage::FLOAT64;
|
||||
fitstype = TDOUBLE;
|
||||
break;
|
||||
default:
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
goto noload;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t size = naxes[0]*naxes[1];
|
||||
size_t w = naxes[0];
|
||||
size_t h = naxes[1];
|
||||
|
||||
info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
|
||||
info.info.append({QObject::tr("Height"), QString::number(naxes[1])});
|
||||
|
||||
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
|
||||
uint8_t *data = static_cast<uint8_t*>(img.data());
|
||||
for (int i=1; i==1 || i<=naxes[2]; i++)
|
||||
{
|
||||
fpixel[2] = i;
|
||||
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
|
||||
if(status)return checkError();
|
||||
}
|
||||
if(fitstype == TSHORT)
|
||||
{
|
||||
uint16_t *s = static_cast<uint16_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT16_MIN;
|
||||
}
|
||||
else if(fitstype == TINT)
|
||||
{
|
||||
uint32_t *s = static_cast<uint32_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT32_MIN;
|
||||
}
|
||||
|
||||
if(img.channels() == 1 || planar)
|
||||
image = std::make_shared<RawImage>(std::move(img));
|
||||
else
|
||||
image = RawImage::fromPlanar(img);
|
||||
}
|
||||
}
|
||||
noload:
|
||||
if(file)
|
||||
{
|
||||
status = loadFITSHeader(file, info);
|
||||
if(status)return checkError();
|
||||
}
|
||||
|
||||
if(image)
|
||||
{
|
||||
for(auto fits : info.fitsHeader)
|
||||
{
|
||||
if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP")
|
||||
image->flip();
|
||||
}
|
||||
}
|
||||
|
||||
fits_close_file(file, &status);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path.toLocal8Bit().data());
|
||||
|
||||
if(index >= (uint32_t)xisf.imagesCount())return false;
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(index);
|
||||
|
||||
auto fitskeywords = xisfImage.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
|
||||
QVector<FITSRecord> xisfWCS;
|
||||
auto imageproperties = xisfImage.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
if(prop.id == "PCL:AstrometricSolution:ReferenceCelestialCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
|
||||
{
|
||||
auto val = prop.value.value<LibXISF::F64Vector>();
|
||||
if(val.size() >= 2)
|
||||
{
|
||||
xisfWCS.append({"CRVAL1", val[0], "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CRVAL2", val[1], "value from PCL:AstrometricSolution"});
|
||||
}
|
||||
}
|
||||
else if(prop.id == "PCL:AstrometricSolution:ReferenceImageCoordinates" && prop.value.type() == LibXISF::Variant::Type::F64Vector)
|
||||
{
|
||||
auto val = prop.value.value<LibXISF::F64Vector>();
|
||||
if(val.size() >= 2)
|
||||
{
|
||||
xisfWCS.append({"CRPIX1", val[0], "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CRPIX2", val[1], "value from PCL:AstrometricSolution"});
|
||||
}
|
||||
}
|
||||
else if(prop.id == "PCL:AstrometricSolution:LinearTransformationMatrix" && prop.value.type() == LibXISF::Variant::Type::F64Matrix)
|
||||
{
|
||||
auto val = prop.value.value<LibXISF::F64Matrix>();
|
||||
if(val.cols() >= 2 && val.rows() >= 2)
|
||||
{
|
||||
xisfWCS.append({"CD1_1", val(0, 0), "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CD1_2", val(0, 1), "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CD2_1", val(1, 0), "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CD2_2", val(1, 1), "value from PCL:AstrometricSolution"});
|
||||
}
|
||||
}
|
||||
else if(prop.id == "PCL:AstrometricSolution:ProjectionSystem")
|
||||
{
|
||||
if(prop.value.toString() == "Gnomonic")
|
||||
{
|
||||
xisfWCS.append({"CTYPE1", "RA---TAN", "value from PCL:AstrometricSolution"});
|
||||
xisfWCS.append({"CTYPE", "DEC--TAN", "value from PCL:AstrometricSolution"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info.num = xisf.imagesCount();
|
||||
info.index = index;
|
||||
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
|
||||
|
||||
auto wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
|
||||
if(!wcs->valid() && xisfWCS.size())wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), xisfWCS);
|
||||
if(wcs->valid())info.wcs = wcs;
|
||||
|
||||
RawImage::DataType type;
|
||||
switch(xisfImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
|
||||
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
|
||||
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
|
||||
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
|
||||
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
LibXISF::Image tmpImage = xisfImage;
|
||||
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
|
||||
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
|
||||
{
|
||||
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
|
||||
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
|
||||
image->setICCProfile(tmpImage.iccProfile());
|
||||
return true;
|
||||
}
|
||||
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
|
||||
{
|
||||
if(planar)
|
||||
{
|
||||
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
|
||||
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize());
|
||||
}
|
||||
else
|
||||
{
|
||||
image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
|
||||
}
|
||||
|
||||
image->setICCProfile(tmpImage.iccProfile());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
info.info.append(QPair<QString, QString>("Error", err.what()));
|
||||
qDebug() << "Failed to load XISF" << err.what();
|
||||
return false;
|
||||
}
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
fitsfile *fr;
|
||||
int status = 0;
|
||||
QString path2 = makeUNCPath(path);
|
||||
fits_open_diskfile(&fr, path2.toLocal8Bit().data(), READONLY, &status);
|
||||
|
||||
if(fr && status == 0)
|
||||
{
|
||||
status = loadFITSHeader(fr, info);
|
||||
fits_close_file(fr, &status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
QString path2 = makeUNCPath(path);
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path2.toLocal8Bit().data());
|
||||
const LibXISF::Image &image = xisf.getImage(0, false);
|
||||
|
||||
auto fitskeywords = image.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
|
||||
auto imageproperties = image.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
info.wcs = std::make_shared<WCSDataT>(image.width(), image.height(), info.fitsHeader);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << err.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag)
|
||||
{
|
||||
char val[1024];
|
||||
ExifEntry *entry = exif_content_get_entry(content, tag);
|
||||
if(entry)
|
||||
{
|
||||
exif_entry_get_value(entry, val, sizeof(val));
|
||||
info.info.append({exif_tag_get_title(tag), val});
|
||||
}
|
||||
}
|
||||
|
||||
bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
|
||||
{
|
||||
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
|
||||
raw->open_file(path.toLocal8Bit().data());
|
||||
raw->imgdata.params.half_size = true;
|
||||
raw->imgdata.params.use_camera_wb = true;
|
||||
raw->imgdata.params.user_flip = 0;
|
||||
if(raw->unpack())
|
||||
return false;
|
||||
|
||||
|
||||
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
|
||||
size_t size = rawdata.sizes.width*rawdata.sizes.height;
|
||||
|
||||
std::vector<uint16_t> out;
|
||||
out.resize(size);
|
||||
size_t d = 0;
|
||||
uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
|
||||
uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
|
||||
size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);
|
||||
|
||||
for(size_t i=rawdata.sizes.top_margin;i<h;i++)
|
||||
{
|
||||
for(size_t o=rawdata.sizes.left_margin;o<w;o++)
|
||||
{
|
||||
uint16_t p = rawdata.raw_image[i*pitch+o];
|
||||
out[d++] = p;
|
||||
}
|
||||
}
|
||||
image = std::make_shared<RawImage>(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16);
|
||||
memcpy(image->data(), &out[0], sizeof(uint16_t)*d);
|
||||
|
||||
QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
|
||||
if(raw->imgdata.other.shutter < 1)
|
||||
{
|
||||
shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
|
||||
}
|
||||
info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)});
|
||||
info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)});
|
||||
info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
|
||||
info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
|
||||
#if LIBRAW_MINOR_VERSION>=19
|
||||
// info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar)
|
||||
{
|
||||
bool ret = false;
|
||||
QElapsedTimer timer;
|
||||
QFileInfo fileInfo(path);
|
||||
timer.start();
|
||||
if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadRAW(path, info, rawImage);
|
||||
qDebug() << "LoadRAW" << timer.elapsed();
|
||||
}
|
||||
else if(isFITS(fileInfo.suffix()))
|
||||
{
|
||||
ret = loadFITS(path, info, rawImage, planar, index);
|
||||
qDebug() << "LoadFITS" << timer.elapsed();
|
||||
}
|
||||
else if(isXISF(fileInfo.suffix()))
|
||||
{
|
||||
ret = loadXISF(path, info, rawImage, planar, index);
|
||||
qDebug() << "LoadXISF" << timer.elapsed();
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img(path);
|
||||
|
||||
ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData());
|
||||
info.info.append({QObject::tr("Width"), QString::number(img.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(img.height())});
|
||||
if(exif)
|
||||
{
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
|
||||
exif_data_free(exif);
|
||||
}
|
||||
rawImage = std::make_shared<RawImage>(img);
|
||||
qDebug() << "LoadQImage" << timer.elapsed();
|
||||
ret = !img.isNull();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool isFITS(const QString &suffix)
|
||||
{
|
||||
return suffix.compare("fits", Qt::CaseInsensitive) == 0 || suffix.compare("fit", Qt::CaseInsensitive) == 0 || suffix.compare("fts", Qt::CaseInsensitive) == 0 || suffix.compare("fz", Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
bool isXISF(const QString &suffix)
|
||||
{
|
||||
return suffix.compare("xisf", Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef LOADIMAGE_H
|
||||
#define LOADIMAGE_H
|
||||
|
||||
#include <QString>
|
||||
#include "imageinfodata.h"
|
||||
|
||||
class RawImage;
|
||||
|
||||
QString makeUNCPath(const QString &path);
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info);
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info);
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar = false);
|
||||
bool isFITS(const QString &suffix);
|
||||
bool isXISF(const QString &suffix);
|
||||
|
||||
#endif // LOADIMAGE_H
|
||||
@@ -0,0 +1,370 @@
|
||||
#include "loadrunable.h"
|
||||
#include "imageringlist.h"
|
||||
#include <QFileInfo>
|
||||
#include <QPainter>
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
#include <fitsio2.h>
|
||||
#include "rawimage.h"
|
||||
#include "loadimage.h"
|
||||
#include <lcms2.h>
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail) :
|
||||
m_file(makeUNCPath(file)),
|
||||
m_receiver(receiver),
|
||||
m_analyzeLevel(level),
|
||||
m_thumbnail(thumbnail),
|
||||
m_index(index)
|
||||
{
|
||||
}
|
||||
|
||||
void LoadRunable::run()
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!m_thumbnail && !m_receiver->isCurrent())
|
||||
{
|
||||
return;
|
||||
}
|
||||
QElapsedTimer timer;
|
||||
ImageInfoData info;
|
||||
QFileInfo finfo(m_file);
|
||||
info.info.append({QObject::tr("Filename"), finfo.fileName()});
|
||||
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(!loadImage(m_file, info, rawImage, m_index))
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")});
|
||||
|
||||
|
||||
if(rawImage && !m_thumbnail)
|
||||
{
|
||||
rawImage->convertToGLFormat();
|
||||
timer.start();
|
||||
rawImage->generateLUT();
|
||||
qDebug() << "generate LUT" << timer.restart();
|
||||
//rawImage->convertTosRGB();
|
||||
//qDebug() << "convert" << timer.restart();
|
||||
rawImage->calcStats();
|
||||
const RawImage::Stats &stats = rawImage->imageStats();
|
||||
qDebug() << "image stats" << timer.restart();
|
||||
if(rawImage->channels() == 1)
|
||||
{
|
||||
info.info.append({QObject::tr("Mean"), QString::number(stats.m_mean[0])});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString::number(stats.m_stdDev[0])});
|
||||
info.info.append({QObject::tr("Median"), QString::number(stats.m_median[0])});
|
||||
info.info.append({QObject::tr("Minimum"), QString::number(stats.m_min[0])});
|
||||
info.info.append({QObject::tr("Maximum"), QString::number(stats.m_max[0])});
|
||||
info.info.append({QObject::tr("MAD"), QString::number(stats.m_mad[0])});
|
||||
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * stats.m_saturated[0] / rawImage->size()) + "%"});
|
||||
}
|
||||
else
|
||||
{
|
||||
info.info.append({QObject::tr("Mean"), QString("%1 %2 %3").arg(stats.m_mean[0]).arg(stats.m_mean[1]).arg(stats.m_mean[2])});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString("%1 %2 %3").arg(stats.m_stdDev[0]).arg(stats.m_stdDev[1]).arg(stats.m_stdDev[2])});
|
||||
info.info.append({QObject::tr("Median"), QString("%1 %2 %3").arg(stats.m_median[0]).arg(stats.m_median[1]).arg(stats.m_median[2])});
|
||||
info.info.append({QObject::tr("Minimum"), QString("%1 %2 %3").arg(stats.m_min[0]).arg(stats.m_min[1]).arg(stats.m_min[2])});
|
||||
info.info.append({QObject::tr("Maximum"), QString("%1 %2 %3").arg(stats.m_max[0]).arg(stats.m_max[1]).arg(stats.m_max[2])});
|
||||
info.info.append({QObject::tr("MAD"), QString("%1 %2 %3").arg(stats.m_mad[0]).arg(stats.m_mad[1]).arg(stats.m_mad[2])});
|
||||
info.info.append({QObject::tr("Saturated"), QString("%1 %2 %3%").arg(100.0 * stats.m_saturated[0] / rawImage->size())
|
||||
.arg(100.0 * stats.m_saturated[1] / rawImage->size())
|
||||
.arg(100.0 * stats.m_saturated[2] / rawImage->size())});
|
||||
}
|
||||
}
|
||||
|
||||
if(m_thumbnail)
|
||||
{
|
||||
if(rawImage && rawImage->valid())
|
||||
{
|
||||
if(QUALITY_RESIZE)
|
||||
rawImage->resize(THUMB_SIZE, THUMB_SIZE);
|
||||
|
||||
rawImage->convertToGLFormat();
|
||||
rawImage->convertToThumbnail();
|
||||
}
|
||||
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
|
||||
}
|
||||
else
|
||||
{
|
||||
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, info));
|
||||
}
|
||||
}
|
||||
catch(std::exception e)
|
||||
{
|
||||
qDebug() << m_file << e.what();
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(m_thumbnail)
|
||||
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
|
||||
else
|
||||
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, ImageInfoData()));
|
||||
}
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams ¶ms, QSemaphore *semaphore) :
|
||||
m_infile(makeUNCPath(in)),
|
||||
m_outfile(makeUNCPath(out)),
|
||||
m_format(format),
|
||||
m_params(params),
|
||||
m_semaphore(semaphore)
|
||||
{
|
||||
}
|
||||
|
||||
void writeFITSImage(fitsfile *fw, std::shared_ptr<RawImage> rawimage, ImageInfoData &imageinfo)
|
||||
{
|
||||
static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"};
|
||||
|
||||
int status = 0;
|
||||
long firstpix[3] = {1,1,1};
|
||||
|
||||
int channels = rawimage->channels();
|
||||
int naxis = channels == 1 ? 2 : 3;
|
||||
long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->channels()};
|
||||
|
||||
std::vector<RawImage> planes;
|
||||
if(channels == 1)
|
||||
planes.push_back(*rawimage);
|
||||
else
|
||||
planes = rawimage->split();
|
||||
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
fits_create_img(fw, BYTE_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TBYTE, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
fits_create_img(fw, USHORT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TUSHORT, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
case RawImage::FLOAT32:
|
||||
fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TFLOAT, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
for(const FITSRecord &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(skipKeys.contains(record.key) || record.xisf)continue;
|
||||
|
||||
bool isdouble;
|
||||
bool isint;
|
||||
bool isbool = record.value.toString() == "T" || record.value.toString() == "F";
|
||||
double vald = record.value.toDouble(&isdouble);
|
||||
int valb = record.value.toString() == "T";
|
||||
long long vall = record.value.toLongLong(&isint);
|
||||
if(isint)isint = vall == vald;
|
||||
QByteArray str = record.value.toString().toLatin1();
|
||||
if(isint)
|
||||
fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isdouble)
|
||||
fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isbool)
|
||||
fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "COMMENT")
|
||||
fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "HISTORY")
|
||||
fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else
|
||||
fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
}
|
||||
}
|
||||
|
||||
void ConvertRunable::run()
|
||||
{
|
||||
QSemaphoreReleaser release;
|
||||
if(m_semaphore)release = QSemaphoreReleaser(m_semaphore);
|
||||
|
||||
ImageInfoData imageinfo;
|
||||
std::shared_ptr<RawImage> rawimage;
|
||||
loadImage(m_infile, imageinfo, rawimage, 0);
|
||||
QFileInfo info(m_outfile);
|
||||
info.dir().mkpath(".");
|
||||
|
||||
if(m_params.autostretch)
|
||||
{
|
||||
rawimage->calcStats();
|
||||
MTFParam mtfParam = rawimage->calcMTFParams();
|
||||
rawimage->applySTF(mtfParam);
|
||||
}
|
||||
if(m_params.binning > 1)
|
||||
{
|
||||
rawimage->resizeInt(m_params.binning, m_params.average);
|
||||
}
|
||||
if(m_params.resize.isValid() && !m_params.resize.isEmpty())
|
||||
{
|
||||
QSize imgSize(rawimage->width(), rawimage->height());
|
||||
imgSize = imgSize.scaled(m_params.resize, m_params.aspect);
|
||||
rawimage->resize(imgSize.width(), imgSize.height());
|
||||
}
|
||||
|
||||
if(rawimage)
|
||||
{
|
||||
if(m_format == "xisf")
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFWriter xisf;
|
||||
int channelCount = rawimage->channels();
|
||||
LibXISF::Image::SampleFormat sampleFormat;
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8: sampleFormat = LibXISF::Image::UInt8; break;
|
||||
case RawImage::UINT16: sampleFormat = LibXISF::Image::UInt16; break;
|
||||
case RawImage::FLOAT32: sampleFormat = LibXISF::Image::Float32; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Planar);
|
||||
if(channelCount == 1)
|
||||
{
|
||||
std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize());
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t off = 0;
|
||||
std::vector<RawImage> planes = rawimage->split();
|
||||
for(const auto &plane : planes)
|
||||
{
|
||||
std::memcpy(image.imageData<uint8_t>() + off, plane.data(), plane.size() * RawImage::typeSize(plane.type()));
|
||||
off += plane.size() * RawImage::typeSize(plane.type());
|
||||
}
|
||||
}
|
||||
for(auto &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(record.xisf)continue;
|
||||
|
||||
if(record.value.typeId() == QMetaType::Bool)
|
||||
image.addFITSKeyword({record.key.toStdString(), record.value.toBool() ? "T" : "F", record.comment.toStdString()});
|
||||
else
|
||||
image.addFITSKeyword({record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});
|
||||
}
|
||||
|
||||
if(m_params.compressionType.startsWith("zstd") && LibXISF::DataBlock::CompressionCodecSupported(LibXISF::DataBlock::ZSTD))
|
||||
image.setCompression(LibXISF::DataBlock::ZSTD, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("lz4hc"))
|
||||
image.setCompression(LibXISF::DataBlock::LZ4HC, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("lz4"))
|
||||
image.setCompression(LibXISF::DataBlock::LZ4, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("zlib"))
|
||||
image.setCompression(LibXISF::DataBlock::Zlib, m_params.compressionLevel);
|
||||
|
||||
if(m_params.compressionType.endsWith("+sh"))
|
||||
image.setByteshuffling(true);
|
||||
|
||||
xisf.writeImage(image);
|
||||
xisf.save(m_outfile.toLocal8Bit().data());
|
||||
}
|
||||
catch(LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << "Failed to save XISF image" << err.what();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(m_format == "fits")
|
||||
{
|
||||
int status = 0;
|
||||
fitsfile *fw;
|
||||
if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile);
|
||||
fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status);
|
||||
if(!m_params.compressionType.isEmpty())
|
||||
{
|
||||
if(m_params.compressionType == "gzip")
|
||||
fits_set_compression_type(fw, GZIP_1, &status);
|
||||
else if(m_params.compressionType == "rice")
|
||||
fits_set_compression_type(fw, RICE_1, &status);
|
||||
}
|
||||
writeFITSImage(fw, rawimage, imageinfo);
|
||||
fits_close_file(fw, &status);
|
||||
return;
|
||||
}
|
||||
|
||||
// if nothing else try QImage
|
||||
{
|
||||
QImage::Format format = QImage::Format_Invalid;
|
||||
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
if(rawimage->channels() == 1)format = QImage::Format_Grayscale8;
|
||||
else if(rawimage->channels() == 3)format = QImage::Format_RGBX8888;
|
||||
else if(rawimage->channels() == 4)format = QImage::Format_RGBA8888;
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
|
||||
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
|
||||
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
|
||||
break;
|
||||
case RawImage::FLOAT16:
|
||||
case RawImage::FLOAT32:
|
||||
case RawImage::FLOAT64:
|
||||
case RawImage::UINT32:
|
||||
rawimage->convertToType(RawImage::UINT16);
|
||||
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
|
||||
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
|
||||
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
|
||||
break;
|
||||
}
|
||||
|
||||
if(format == QImage::Format_Invalid)return;
|
||||
|
||||
QImage qimage((const uchar*)rawimage->data(), rawimage->width(), rawimage->height(), rawimage->widthBytes(), format);
|
||||
qimage.save(m_outfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertParams::ConvertParams(const QVariantMap &map)
|
||||
{
|
||||
bool ok = false;
|
||||
if(map.contains("compressionLevel"))
|
||||
compressionLevel = std::clamp(map["compressionLevel"].toInt(&ok), -1, 100);
|
||||
|
||||
if(!ok)compressionLevel = -1;
|
||||
|
||||
if(map.contains("compressionType"))
|
||||
compressionType = map["compressionType"].toString();
|
||||
|
||||
if(map.contains("binning"))
|
||||
binning = map["binning"].toInt();
|
||||
|
||||
if(map.contains("average"))
|
||||
average = map["average"].toBool();
|
||||
|
||||
if(map.contains("resize"))
|
||||
{
|
||||
QVariantMap size = map["resize"].toMap();
|
||||
if(size.contains("width") && size.contains("height"))
|
||||
{
|
||||
int w = size["width"].toInt();
|
||||
int h = size["height"].toInt();
|
||||
resize = QSize(w, h);
|
||||
}
|
||||
if(size.contains("aspect"))
|
||||
{
|
||||
QString aspectStr = map["aspect"].toString();
|
||||
if(aspectStr == "keep")
|
||||
aspect = Qt::KeepAspectRatio;
|
||||
else if(aspectStr == "expand")
|
||||
aspect = Qt::KeepAspectRatioByExpanding;
|
||||
else if(aspectStr == "ignore")
|
||||
aspect = Qt::IgnoreAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
if(map.contains("autostretch"))
|
||||
autostretch = map["autostretch"].toBool();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef LOADRUNABLE_H
|
||||
#define LOADRUNABLE_H
|
||||
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
#include <QSemaphore>
|
||||
#include <QSize>
|
||||
#include "imageinfodata.h"
|
||||
|
||||
class Image;
|
||||
|
||||
class LoadRunable : public QRunnable
|
||||
{
|
||||
QString m_file;
|
||||
Image *m_receiver;
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
bool m_thumbnail;
|
||||
int m_index = 0;
|
||||
public:
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail = false);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
class ConvertRunable : public QRunnable
|
||||
{
|
||||
public:
|
||||
struct ConvertParams
|
||||
{
|
||||
int compressionLevel = -1;
|
||||
QString compressionType;
|
||||
int binning = 0;
|
||||
bool average = true;
|
||||
QSize resize;
|
||||
Qt::AspectRatioMode aspect = Qt::KeepAspectRatio;
|
||||
bool autostretch = false;
|
||||
ConvertParams(){}
|
||||
ConvertParams(const QVariantMap &map);
|
||||
};
|
||||
ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams ¶ms = ConvertParams(), QSemaphore *semaphore = nullptr);
|
||||
void run() override;
|
||||
private:
|
||||
QString m_infile;
|
||||
QString m_outfile;
|
||||
QString m_format;
|
||||
ConvertParams m_params;
|
||||
QSemaphore *m_semaphore;
|
||||
};
|
||||
|
||||
#endif // LOADRUNABLE_H
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QTranslator>
|
||||
#include <QCommandLineParser>
|
||||
#include <QSettings>
|
||||
#include <stdlib.h>
|
||||
#include "../thumbnailer/genthumbnail.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifdef __linux__
|
||||
setenv("LC_NUMERIC", "C", 1);
|
||||
#endif
|
||||
|
||||
#if defined(__i386__) || defined(__x86_64__) || defined(__APPLE__)
|
||||
bool useGLES = false;
|
||||
#else
|
||||
bool useGLES = true;
|
||||
#endif
|
||||
|
||||
QCommandLineParser cmd;
|
||||
cmd.addOption({"gl", "Use desktop OpenGL. This is default on x86 and MacOS platform."});
|
||||
cmd.addOption({"gles", "Use OpenGL ES. This is default on ARM platform."});
|
||||
cmd.addOption({{"thumb", "thumbnail"}, "Generate thumbnail and save it to path.", "path"});
|
||||
cmd.addOption({{"s", "size"}, "Size of the thumbnails in pixels (default: 128)", "size", "128"});
|
||||
cmd.addPositionalArgument("file", "File to open");
|
||||
cmd.addHelpOption();
|
||||
QStringList cmdArgs;
|
||||
for(int i = 0; i < argc; i++)
|
||||
cmdArgs.append(argv[i]);
|
||||
|
||||
cmd.process(cmdArgs);
|
||||
if(cmd.isSet("gl"))
|
||||
useGLES = false;
|
||||
if(cmd.isSet("gles"))
|
||||
useGLES = true;
|
||||
|
||||
if(cmd.isSet("thumb"))
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QStringList files = cmd.positionalArguments();
|
||||
if(files.size() == 0)
|
||||
return 1;
|
||||
|
||||
QString thumb = cmd.value("thumb");
|
||||
int size = 128;
|
||||
bool ok;
|
||||
int size2 = cmd.value("s").toInt(&ok);
|
||||
if(ok)
|
||||
size = size2;
|
||||
|
||||
return generateThumbnail(files.front(), thumb, size);
|
||||
}
|
||||
|
||||
QSurfaceFormat format;
|
||||
if(useGLES)
|
||||
{
|
||||
format.setMajorVersion(3);
|
||||
format.setMinorVersion(0);
|
||||
format.setRenderableType(QSurfaceFormat::OpenGLES);
|
||||
}
|
||||
else
|
||||
{
|
||||
format.setMajorVersion(3);
|
||||
format.setMinorVersion(3);
|
||||
//format.setOption(QSurfaceFormat::DebugContext);
|
||||
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
}
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setOrganizationName("nou");
|
||||
a.setApplicationName("Tenmon");
|
||||
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
|
||||
|
||||
QTranslator translator;
|
||||
QTranslator translator2;
|
||||
QSettings settings;
|
||||
QString lang = settings.value("settings/lang").toString();
|
||||
if(lang.isEmpty())
|
||||
{
|
||||
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
|
||||
a.installTranslator(&translator);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(translator.load("tenmon_" + lang, ":/translations"))
|
||||
a.installTranslator(&translator);
|
||||
}
|
||||
|
||||
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
|
||||
a.installTranslator(&translator2);
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
if(!cmd.positionalArguments().isEmpty())
|
||||
{
|
||||
QStringList files = cmd.positionalArguments();
|
||||
QStringList paths;
|
||||
for(auto &arg : files)
|
||||
{
|
||||
QUrl url(arg);
|
||||
QFileInfo info(url.isLocalFile() ? url.toLocalFile() : arg);
|
||||
if(info.exists())
|
||||
paths.append(info.canonicalFilePath());
|
||||
}
|
||||
if(paths.size() == 1)
|
||||
w.loadFile(paths.front());
|
||||
else if(paths.size() > 1)
|
||||
w.loadFiles(paths);
|
||||
}
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QProgressDialog>
|
||||
#include <QDebug>
|
||||
#include <QDockWidget>
|
||||
#include <QActionGroup>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <QSettings>
|
||||
@@ -17,12 +18,19 @@
|
||||
#include <QThreadPool>
|
||||
#include <QStatusBar>
|
||||
#include <QImageReader>
|
||||
#include <QImageWriter>
|
||||
#include <QMimeDatabase>
|
||||
#include <QDesktopServices>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include "loadrunable.h"
|
||||
#include "markedfiles.h"
|
||||
#include "about.h"
|
||||
#include "statusbar.h"
|
||||
#include "settingsdialog.h"
|
||||
#include "histogram.h"
|
||||
#include "batchprocessing.h"
|
||||
#include "filemanager.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/ioctl.h>
|
||||
@@ -37,7 +45,7 @@ int MainWindow::socketPair[2] = {0, 0};
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
qRegisterMetaType<ImageInfoData>("ImageInfoData");
|
||||
qRegisterMetaType<RawImage*>("RawImage");
|
||||
qRegisterMetaType<std::shared_ptr<RawImage>>("std::shared_ptr<RawImage>");
|
||||
|
||||
SettingsDialog::loadSettings();
|
||||
|
||||
@@ -50,54 +58,90 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
for(auto format : supportedFormats)
|
||||
{
|
||||
QMimeType mimeType = db.mimeTypeForName(format);
|
||||
_saveFilter.append(mimeType.filterString() + ";;");
|
||||
_openFilter.append("*.");
|
||||
_openFilter.append(mimeType.suffixes().join(" *."));
|
||||
_openFilter.append(" ");
|
||||
nameFilter.append(mimeType.suffixes());
|
||||
}
|
||||
_openFilter.append("*.fit *.fits *.xisf *.cr2 *.nef *.dng)");
|
||||
auto supportedWrite = QImageWriter::supportedMimeTypes();
|
||||
for(auto format : supportedWrite)
|
||||
{
|
||||
QMimeType mimeType = db.mimeTypeForName(format);
|
||||
_saveFilter.append(mimeType.filterString() + ";;");
|
||||
}
|
||||
|
||||
_openFilter.append("*.fit *.fits *.fts *.fz *.xisf *.cr2 *.cr3 *.nef *.dng)");
|
||||
_openFilter.append(tr(";;All files (*)"));
|
||||
nameFilter.append({"fit", "fits", "xisf", "cr2", "nef", "dng"});
|
||||
nameFilter.append({"fit", "fits", "fts", "fz", "xisf", "cr2", "cr3", "nef", "dng"});
|
||||
QImageReader::setAllocationLimit(0);
|
||||
_openSuffix = {nameFilter.constBegin(), nameFilter.constEnd()};
|
||||
|
||||
m_info = new ImageInfo(this);
|
||||
QDockWidget *infoDock = new QDockWidget(tr("Image info"), this);
|
||||
infoDock->setWidget(m_info);
|
||||
infoDock->setObjectName("infoDock");
|
||||
addDockWidget(Qt::LeftDockWidgetArea, infoDock);
|
||||
resize(800, 600);
|
||||
resize(1024, 600);
|
||||
setStatusBar(new QStatusBar(this));
|
||||
|
||||
m_database = new Database(this);
|
||||
if(!m_database->init())
|
||||
QMessageBox::critical(this, tr("Can't open DB"), tr("Can't open SQLITE database"));
|
||||
|
||||
m_imageGL = new ImageScrollAreaGL(m_database, this);
|
||||
setCentralWidget(m_imageGL);
|
||||
m_image = new ImageScrollArea(m_database, this);
|
||||
setCentralWidget(m_image);
|
||||
|
||||
StatusBar *statusBar = new StatusBar(this);
|
||||
setStatusBar(statusBar);
|
||||
connect(m_imageGL->imageWidget(), &ImageWidget::status, statusBar, &StatusBar::newStatus);
|
||||
connect(m_image, &ImageScrollArea::status, statusBar, &StatusBar::newStatus);
|
||||
|
||||
m_stretchPanel = new StretchToolbar(this);
|
||||
connect(m_stretchPanel, SIGNAL(paramChanged(float,float,float)), m_imageGL->imageWidget(), SLOT(setMTFParams(float,float,float)));
|
||||
connect(m_stretchPanel, &StretchToolbar::paramChanged, m_image, &ImageScrollArea::setMTFParams);
|
||||
connect(m_stretchPanel, &StretchToolbar::autoStretch, [&](){ m_stretchPanel->stretchImage(m_ringList->currentImage().get()); });
|
||||
connect(m_stretchPanel, &StretchToolbar::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
|
||||
connect(m_stretchPanel, &StretchToolbar::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
|
||||
connect(m_stretchPanel, &StretchToolbar::invert, m_image, &ImageScrollArea::invert);
|
||||
connect(m_stretchPanel, &StretchToolbar::superPixel, m_image, &ImageScrollArea::superPixel);
|
||||
connect(m_stretchPanel, &StretchToolbar::falseColor, m_image, &ImageScrollArea::falseColor);
|
||||
connect(m_stretchPanel, &StretchToolbar::drawGrid, m_image, &ImageScrollArea::drawGrid);
|
||||
|
||||
m_ringList = new ImageRingList(m_database, nameFilter, this);
|
||||
m_filesystem = new FilesystemWidget(m_ringList, this);
|
||||
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
|
||||
connect(m_filesystem, &FilesystemWidget::fileSelected, this, static_cast<void (MainWindow::*)(int)>(&MainWindow::loadFile));
|
||||
connect(m_filesystem, &FilesystemWidget::sortChanged, m_ringList, &ImageRingList::setSort);
|
||||
connect(m_filesystem, &FilesystemWidget::reverseSort, m_ringList, &ImageRingList::reverseSort);
|
||||
|
||||
m_filetree = nullptr;
|
||||
#if !defined(FLATPAK) || !defined(__aarch64__)//bug with QTreeView and QFileSystemModel on ARM64 under flatpak
|
||||
m_filetree = new Filetree(this);
|
||||
connect(m_filetree, &Filetree::fileSelected, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
connect(m_filetree, &Filetree::copyFiles, [this](const QString &path){ copyOrMove(true, path); });
|
||||
connect(m_filetree, &Filetree::moveFiles, [this](const QString &path){ copyOrMove(false, path); });
|
||||
connect(m_filetree, &Filetree::indexDirectory, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::indexDir));
|
||||
#endif
|
||||
|
||||
m_databaseView = new DataBaseView(m_database, this);
|
||||
connect(m_databaseView, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
|
||||
connect(m_databaseView, &DataBaseView::loadFile, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
|
||||
#ifdef PLATESOLVER
|
||||
_plateSolving = new PlateSolving(this);
|
||||
addDockWidget(Qt::RightDockWidgetArea, _plateSolving);
|
||||
_plateSolving->hide();
|
||||
#endif
|
||||
|
||||
QToolBar *navigationToolbar = new QToolBar(tr("Navigation toolbar"), this);
|
||||
navigationToolbar->setObjectName("navigationtoolbar");
|
||||
navigationToolbar->hide();
|
||||
QAction *prevAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowLeft), tr("Previous image"));
|
||||
prevAction->setShortcuts({Qt::Key_Left, Qt::Key_Up});
|
||||
QAction *nextAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("Next image"));
|
||||
nextAction->setShortcuts({Qt::Key_Right, Qt::Key_Down});
|
||||
QAction *prevSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowUp), tr("Prev sub image"), Qt::Key_PageUp);
|
||||
QAction *nextSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowDown), tr("Next sub image"), Qt::Key_PageDown);
|
||||
connect(prevAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::decrement));
|
||||
connect(nextAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
|
||||
connect(prevSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::prevSubImage));
|
||||
connect(nextSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::nextSubImage));
|
||||
|
||||
addToolBar(Qt::TopToolBarArea, navigationToolbar);
|
||||
addToolBar(Qt::TopToolBarArea, m_stretchPanel);
|
||||
|
||||
QDockWidget *filesystemDock = new QDockWidget(tr("Filesystem"), this);
|
||||
@@ -111,37 +155,61 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
databaseViewDock->hide();
|
||||
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
|
||||
|
||||
QDockWidget *filetreeDock = new QDockWidget(tr("File tree"), this);
|
||||
QDockWidget *filetreeDock = nullptr;
|
||||
#if !defined(FLATPAK) || !defined(__aarch64__)
|
||||
filetreeDock = new QDockWidget(tr("File tree"), this);
|
||||
filetreeDock->setWidget(m_filetree);
|
||||
filetreeDock->setObjectName("filetreeDock");
|
||||
databaseViewDock->hide();
|
||||
addDockWidget(Qt::LeftDockWidgetArea, filetreeDock);
|
||||
#endif
|
||||
|
||||
Histogram *histogram = new Histogram(this);
|
||||
QDockWidget *histogramDock = new QDockWidget(tr("Histogram"), this);
|
||||
histogramDock->setWidget(histogram);
|
||||
histogramDock->setObjectName("histogramDock");
|
||||
histogramDock->hide();
|
||||
addDockWidget(Qt::LeftDockWidgetArea, histogramDock);
|
||||
|
||||
setWindowTitle(tr("Tenmon"));
|
||||
|
||||
connect(m_ringList, SIGNAL(pixmapLoaded(Image*)), this, SLOT(pixmapLoaded(Image*)));
|
||||
connect(m_ringList, SIGNAL(currentImageChanged(int)), this, SLOT(updateWindowTitle()));
|
||||
connect(m_ringList, SIGNAL(infoLoaded(ImageInfoData)), m_info, SLOT(setInfo(const ImageInfoData&)));
|
||||
connect(m_ringList, SIGNAL(currentImageChanged(int)), m_filesystem, SLOT(selectFile(int)));
|
||||
connect(m_ringList, &ImageRingList::thumbnailLoaded, m_imageGL->imageWidget(), &ImageWidget::thumbnailLoaded);
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, m_image, &ImageScrollArea::imageLoaded);
|
||||
connect(m_ringList, &ImageRingList::currentImageChanged, this, &MainWindow::updateWindowTitle);
|
||||
connect(m_ringList, &ImageRingList::infoLoaded, m_info, &ImageInfo::setInfo);
|
||||
connect(m_ringList, &ImageRingList::currentImageChanged, m_filesystem, &FilesystemWidget::selectFile);
|
||||
connect(m_ringList, &ImageRingList::thumbnailLoaded, m_image, &ImageScrollArea::thumbnailLoaded);
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, m_stretchPanel, &StretchToolbar::imageLoaded);
|
||||
connect(m_imageGL->imageWidget(), &ImageWidget::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, histogram, &Histogram::imageLoaded);
|
||||
#ifdef PLATESOLVER
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, _plateSolving, &PlateSolving::imageLoaded);
|
||||
connect(_plateSolving, &PlateSolving::headerUpdated, m_ringList, &ImageRingList::reloadImage);
|
||||
#endif
|
||||
connect(m_image, &ImageScrollArea::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
|
||||
QMenu *fileMenu = new QMenu(tr("File"), this);
|
||||
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence::Open);
|
||||
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence::Save);
|
||||
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, static_cast<void (MainWindow::*)()>(&MainWindow::loadFile));
|
||||
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
|
||||
QAction *saveAs = fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, &MainWindow::saveAs);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()), Qt::Key_F5);
|
||||
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()), Qt::Key_F6);
|
||||
fileMenu->addAction(tr("Move marked files to trash"), this, &MainWindow::deleteMarked, QKeySequence::Delete);
|
||||
#if !defined(FLATPAK) || !defined(__aarch64__)
|
||||
fileMenu->addAction(tr("File manager"), this, &MainWindow::openFileManager);
|
||||
#endif
|
||||
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, &MainWindow::copyMarked);
|
||||
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, &MainWindow::moveMarked);
|
||||
fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
||||
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
||||
fileMenu->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir));
|
||||
fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex);
|
||||
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
||||
fileMenu->addAction(tr("Batch processing"), Qt::Key_B | Qt::CTRL, [this](){
|
||||
BatchProcessing *batchProcessing = new BatchProcessing(m_database, this);
|
||||
batchProcessing->exec();
|
||||
delete batchProcessing;
|
||||
});
|
||||
fileMenu->addSeparator();
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, &MainWindow::liveMode);
|
||||
liveModeAction->setCheckable(true);
|
||||
QAction *exitAction = fileMenu->addAction(tr("Exit"), this, SLOT(close()));
|
||||
QAction *exitAction = fileMenu->addAction(tr("Exit"), QCoreApplication::instance(), &QCoreApplication::quit, Qt::QueuedConnection);
|
||||
exitAction->setShortcut(QKeySequence::Quit);
|
||||
menuBar()->addMenu(fileMenu);
|
||||
|
||||
@@ -149,32 +217,89 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
editMenu->addAction(tr("Settings"), this, &MainWindow::showSettingsDialog);
|
||||
menuBar()->addMenu(editMenu);
|
||||
|
||||
QMenu *navigationMenu = new QMenu(tr("Navigation"), this);
|
||||
navigationMenu->addAction(prevAction);
|
||||
navigationMenu->addAction(nextAction);
|
||||
navigationMenu->addAction(prevSubAction);
|
||||
navigationMenu->addAction(nextSubAction);
|
||||
menuBar()->addMenu(navigationMenu);
|
||||
|
||||
QMenu *viewMenu = new QMenu(tr("View"), this);
|
||||
viewMenu->addAction(tr("Zoom In"), m_imageGL, SLOT(zoomIn()), QKeySequence::ZoomIn);
|
||||
viewMenu->addAction(tr("Zoom Out"), m_imageGL, SLOT(zoomOut()), QKeySequence::ZoomOut);
|
||||
viewMenu->addAction(tr("Best Fit"), m_imageGL, SLOT(bestFit()), QKeySequence("Ctrl+1"));
|
||||
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
|
||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), [this](bool checked){
|
||||
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn, m_image, &ImageScrollArea::zoomIn);
|
||||
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut, m_image, &ImageScrollArea::zoomOut);
|
||||
viewMenu->addAction(tr("Best Fit"), QKeySequence("Ctrl+1"), m_image, &ImageScrollArea::bestFit);
|
||||
viewMenu->addAction(tr("100%"), QKeySequence("Ctrl+0"), m_image, &ImageScrollArea::oneToOne);
|
||||
viewMenu->addSeparator();
|
||||
QMenu *bayerMenu = viewMenu->addMenu(tr("Bayer mask"));
|
||||
QActionGroup *bayerActionGroup = new QActionGroup(this);
|
||||
QAction *rggbAction = bayerActionGroup->addAction(tr("RGGB"));//0 0
|
||||
QAction *grbgAction = bayerActionGroup->addAction(tr("GRBG"));//1 0
|
||||
QAction *gbrgAction = bayerActionGroup->addAction(tr("GBRG"));//0 1
|
||||
QAction *bggrAction = bayerActionGroup->addAction(tr("BGGR"));//1 1
|
||||
rggbAction->setCheckable(true); rggbAction->setData(0); rggbAction->setIcon(QIcon(":/bayer.png"));
|
||||
grbgAction->setCheckable(true); grbgAction->setData(1); grbgAction->setIcon(QIcon(":/grbg.png"));
|
||||
gbrgAction->setCheckable(true); gbrgAction->setData(2); gbrgAction->setIcon(QIcon(":/gbrg.png"));
|
||||
bggrAction->setCheckable(true); bggrAction->setData(3); bggrAction->setIcon(QIcon(":/bggr.png"));
|
||||
bayerMenu->addActions({rggbAction, grbgAction, gbrgAction, bggrAction});
|
||||
viewMenu->addMenu(bayerMenu);
|
||||
connect(bayerActionGroup, &QActionGroup::triggered, [this](QAction *action){
|
||||
int data = action->data().toInt();
|
||||
m_image->setBayerMask(data);
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/bayermask", data);
|
||||
});
|
||||
|
||||
QStringList colormaps = {"Autumn", "Bone", "Jet", "Winter", "Rainbow", "Ocean", "Summer", "Spring", "Cool", "HSV", "Pink", "Hot", "Parula", "Magma",
|
||||
"Inferno", "Plasma", "Viridis", "Cividis", "Twilight", "Twilight shifted", "Turbo", "Deepgreen"};
|
||||
QMenu *colormapMenu = viewMenu->addMenu(tr("Colormap"));
|
||||
QActionGroup *colormapActionGroup = new QActionGroup(this);
|
||||
|
||||
QImage cmImg = ImageWidget::loadColormap();
|
||||
for(int i=0; i<cmImg.height(); i++)
|
||||
{
|
||||
QImage icon = cmImg.copy(0, i, cmImg.width(), 1).scaled(32, 32);
|
||||
QAction *action = colormapActionGroup->addAction(i < colormaps.size() ? colormaps[i] : tr("User %1").arg(i - colormaps.size() + 1));
|
||||
action->setIcon(QPixmap::fromImage(icon));
|
||||
action->setCheckable(true); action->setData(i);
|
||||
colormapMenu->addAction(action);
|
||||
}
|
||||
viewMenu->addMenu(colormapMenu);
|
||||
connect(colormapActionGroup, &QActionGroup::triggered, [this](QAction *action){
|
||||
int data = action->data().toInt();
|
||||
m_image->setColormap(data);
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/colormap", data);
|
||||
});
|
||||
|
||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), Qt::Key_F2, [this](bool checked){
|
||||
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
||||
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
|
||||
m_imageGL->imageWidget()->showThumbnail(checked);
|
||||
m_imageGL->setThumbnails(checked ? m_ringList->imageCount() : 0);
|
||||
m_image->allocateThumbnails(m_ringList->imageNames());
|
||||
m_image->showThumbnail(checked);
|
||||
if(checked)m_ringList->loadThumbnails();
|
||||
else m_ringList->stopLoading();
|
||||
}, Qt::Key_F2);
|
||||
});
|
||||
thumbnailsAction->setCheckable(true);
|
||||
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), Qt::Key_F3, m_ringList, &ImageRingList::toggleSlideshow);
|
||||
slideshowAction->setCheckable(true);
|
||||
viewMenu->addSeparator();
|
||||
auto actionList = m_stretchPanel->actions();
|
||||
actionList.removeFirst();
|
||||
viewMenu->addActions(actionList);
|
||||
menuBar()->addMenu(viewMenu);
|
||||
|
||||
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
||||
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F7);
|
||||
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
|
||||
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, &MainWindow::markImage);
|
||||
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, &MainWindow::unmarkImage);
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
|
||||
selectMenu->addAction(tr("Unmark and next"), this, SLOT(unmarkAndNext()), Qt::Key_X);
|
||||
selectMenu->addAction(tr("Show marked"), this, &MainWindow::showMarkFilesDialog);
|
||||
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, &MainWindow::markAndNext);
|
||||
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, &MainWindow::unmarkAndNext);
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Show marked list"), this, &MainWindow::showMarkFilesDialog);
|
||||
QAction *openMarked = selectMenu->addAction(tr("Open marked"), m_ringList, &ImageRingList::setMarked);
|
||||
menuBar()->addMenu(selectMenu);
|
||||
fileMenu->insertAction(saveAs, openMarked);
|
||||
|
||||
QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
||||
/*QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
||||
QActionGroup *analyzeGroup = new QActionGroup(this);
|
||||
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
|
||||
static QAction* lastAction = nullptr;
|
||||
@@ -197,25 +322,50 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
connect(peakAction, SIGNAL(toggled(bool)), this, SLOT(peakFinder(bool)));
|
||||
connect(starAction, SIGNAL(toggled(bool)), this, SLOT(starFinder(bool)));
|
||||
analyzeMenu->addActions({statsAction, peakAction, starAction});
|
||||
menuBar()->addMenu(analyzeMenu);
|
||||
menuBar()->addMenu(analyzeMenu);*/
|
||||
|
||||
QMenu *dockMenu = new QMenu(tr("Docks"), this);
|
||||
dockMenu->addAction(infoDock->toggleViewAction());
|
||||
dockMenu->addAction(m_stretchPanel->toggleViewAction());
|
||||
dockMenu->addAction(navigationToolbar->toggleViewAction());
|
||||
dockMenu->addAction(filesystemDock->toggleViewAction());
|
||||
dockMenu->addAction(databaseViewDock->toggleViewAction());
|
||||
dockMenu->addAction(filetreeDock->toggleViewAction());
|
||||
if(filetreeDock)dockMenu->addAction(filetreeDock->toggleViewAction());
|
||||
dockMenu->addAction(histogramDock->toggleViewAction());
|
||||
#ifdef PLATESOLVER
|
||||
dockMenu->addAction(_plateSolving->toggleViewAction());
|
||||
#endif
|
||||
menuBar()->addMenu(dockMenu);
|
||||
|
||||
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
|
||||
helpMenu->addAction(tr("Help"), [this]{ HelpDialog help(this); help.exec(); }, QKeySequence::HelpContents);
|
||||
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog *help = new HelpDialog(this); help->show(); });
|
||||
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
|
||||
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
|
||||
helpMenu->addAction(tr("Check for update"), this, &MainWindow::checkNewVersion);
|
||||
|
||||
setupSigterm();
|
||||
QSettings settings;
|
||||
restoreGeometry(settings.value("mainwindow/geometry").toByteArray());
|
||||
restoreState(settings.value("mainwindow/state").toByteArray());
|
||||
int bayermask = settings.value("mainwindow/bayermask", 0).toInt();
|
||||
switch(bayermask)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
rggbAction->setChecked(true); break;
|
||||
case 1:
|
||||
grbgAction->setChecked(true); break;
|
||||
case 2:
|
||||
gbrgAction->setChecked(true); break;
|
||||
case 3:
|
||||
bggrAction->setChecked(true); break;
|
||||
}
|
||||
int colormap = settings.value("mainwindow/colormap", 4).toInt();
|
||||
if(colormap >= 0 && colormap < colormapActionGroup->actions().size())
|
||||
colormapActionGroup->actions().at(colormap)->setChecked(true);
|
||||
|
||||
m_image->setBayerMask(bayermask);
|
||||
m_image->setColormap(colormap);
|
||||
|
||||
QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
|
||||
if(standardLocations.size())
|
||||
@@ -223,22 +373,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
|
||||
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
|
||||
|
||||
QStringList args = QCoreApplication::arguments();
|
||||
args.removeFirst();
|
||||
for(auto &arg : args)
|
||||
{
|
||||
QFileInfo info(arg);
|
||||
if(info.exists())
|
||||
{
|
||||
m_ringList->setFile(info.canonicalFilePath());
|
||||
updateWindowTitle();
|
||||
_lastDir = info.absoluteDir().absolutePath();
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_imageGL->setFocus();
|
||||
m_image->setFocus();
|
||||
|
||||
// workaround for nasty wayland backend bug https://bugreports.qt.io/browse/QTBUG-87332
|
||||
if(static_cast<QGuiApplication*>(QCoreApplication::instance())->platformName() == "wayland")
|
||||
@@ -246,7 +381,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
infoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filesystemDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
databaseViewDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
if(filetreeDock)filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
if(_plateSolving)_plateSolving->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
m_stretchPanel->setFloatable(false);
|
||||
}
|
||||
}
|
||||
@@ -256,32 +392,6 @@ MainWindow::~MainWindow()
|
||||
delete m_database;
|
||||
}
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_Left:
|
||||
case Qt::Key_Up:
|
||||
m_ringList->decrement();
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
case Qt::Key_Down:
|
||||
m_ringList->increment();
|
||||
break;
|
||||
default:
|
||||
event->ignore();
|
||||
break;
|
||||
}
|
||||
|
||||
if(event->isAccepted())
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
void MainWindow::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void MainWindow::setupSigterm()
|
||||
{
|
||||
#ifdef __linux__
|
||||
@@ -298,7 +408,7 @@ void MainWindow::setupSigterm()
|
||||
|
||||
::socketpair(AF_UNIX, SOCK_STREAM, 0, socketPair);
|
||||
socketNotifier = new QSocketNotifier(socketPair[1], QSocketNotifier::Read, this);
|
||||
connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(socketNotify()));
|
||||
connect(socketNotifier, &QSocketNotifier::activated, this, &MainWindow::socketNotify);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -331,6 +441,9 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
if(!dest.isEmpty() && dir.exists())
|
||||
{
|
||||
int i = 0;
|
||||
int missing = 0;
|
||||
bool overwriteAll = false;
|
||||
bool skipAll = false;
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
QProgressDialog progress(copy ? tr("Copying") : tr("Moving"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
@@ -342,8 +455,42 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
QFile srcFile(file);
|
||||
QFile dstFile(dir.absoluteFilePath(info.fileName()));
|
||||
|
||||
if(dstFile.exists())
|
||||
if(!srcFile.exists())
|
||||
{
|
||||
missing++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(dstFile.exists())
|
||||
{
|
||||
if(skipAll)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if(overwriteAll)
|
||||
{
|
||||
dstFile.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::StandardButton button = QMessageBox::question(this, tr("Overwrite file?"), tr("Destination file %1 already exists. Overwrite?").arg(dstFile.fileName()),
|
||||
QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll);
|
||||
switch (button)
|
||||
{
|
||||
case QMessageBox::YesToAll:
|
||||
overwriteAll = true;
|
||||
case QMessageBox::Yes:
|
||||
dstFile.remove();
|
||||
break;
|
||||
case QMessageBox::NoToAll:
|
||||
skipAll = true;
|
||||
case QMessageBox::No:
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(progress.wasCanceled())
|
||||
return;
|
||||
@@ -382,6 +529,8 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
progress.setValue(i++);
|
||||
}
|
||||
m_database->clearMarkedFiles();
|
||||
if(missing)
|
||||
QMessageBox::information(this, tr("Missing marked files"), tr("%1 marked files were missing. They were skipped.").arg(missing));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,14 +543,6 @@ void MainWindow::socketNotify()
|
||||
socketNotifier->setEnabled(true);
|
||||
}
|
||||
|
||||
void MainWindow::pixmapLoaded(Image *image)
|
||||
{
|
||||
if(image->rawImage())
|
||||
{
|
||||
m_imageGL->setImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadFile()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this,
|
||||
@@ -424,14 +565,35 @@ void MainWindow::loadFile(const QString &path)
|
||||
_lastDir = info.canonicalPath();
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
if(settings.value("settings/bestfit", false).toBool())
|
||||
m_image->bestFit();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadFiles(const QStringList &paths)
|
||||
{
|
||||
m_ringList->setFiles(paths);
|
||||
}
|
||||
|
||||
void MainWindow::loadFile(int row)
|
||||
{
|
||||
m_ringList->loadFile(row);
|
||||
}
|
||||
|
||||
void MainWindow::loadDir()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this,
|
||||
tr("Open directory recursively"),
|
||||
_lastDir);
|
||||
if(!dir.isEmpty())
|
||||
{
|
||||
_lastDir = dir;
|
||||
m_ringList->setDir(dir, QString(), true);
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::indexDir()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir, QFileDialog::ShowDirsOnly);
|
||||
@@ -463,32 +625,36 @@ void MainWindow::saveAs()
|
||||
_lastDir,
|
||||
_saveFilter,
|
||||
&selectedFilter);
|
||||
auto filterToFormat = [](const QString &file, const QString &filter) -> const char*
|
||||
auto filterToFormat = [](const QString &file, const QString &filter) -> const QString
|
||||
{
|
||||
QString suffix = QFileInfo(file).suffix();
|
||||
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "JPEG";
|
||||
if(!suffix.compare("png", Qt::CaseInsensitive))return "PNG";
|
||||
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "FITS";
|
||||
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "XISF";
|
||||
if(filter.contains("png"))return "PNG";
|
||||
if(filter.contains("fits"))return "FITS";
|
||||
if(filter.contains("xisf"))return "XISF";
|
||||
return "JPEG";
|
||||
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "jpeg";
|
||||
if(!suffix.compare("png", Qt::CaseInsensitive))return "png";
|
||||
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "fits";
|
||||
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "xisf";
|
||||
if(filter.contains("png"))return "png";
|
||||
if(filter.contains("fits"))return "fits";
|
||||
if(filter.contains("xisf"))return "xisf";
|
||||
QRegularExpression suf("\\(\\*\\.([a-zA-Z]+).*\\)");
|
||||
auto match = suf.match(filter);
|
||||
if(match.hasMatch())
|
||||
return match.captured(1);
|
||||
return "jpeg";
|
||||
};
|
||||
|
||||
if(!file.isEmpty())
|
||||
{
|
||||
QString format = filterToFormat(file, selectedFilter);
|
||||
|
||||
if(format == "FITS" || format == "XISF")
|
||||
if(format == "fits" || format == "xisf")
|
||||
{
|
||||
convert(file, format);
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img = m_imageGL->imageWidget()->renderToImage();
|
||||
QImage img = m_image->renderToImage();
|
||||
if(!img.isNull())
|
||||
img.save(file, filterToFormat(file, selectedFilter));
|
||||
img.save(file, filterToFormat(file, selectedFilter).toLatin1().data());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,13 +795,60 @@ void MainWindow::exportCSV()
|
||||
m_databaseView->exportCSV(file);
|
||||
}
|
||||
|
||||
void MainWindow::checkNewVersion()
|
||||
{
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
|
||||
QNetworkRequest request(QUrl("https://gitea.nouspiro.space/api/v1/repos/nou/tenmon/releases/latest"));
|
||||
request.setRawHeader("accept", "application/json");
|
||||
QNetworkReply *reply = manager->get(request);
|
||||
connect(reply, &QNetworkReply::finished, [this, manager, reply](){
|
||||
QJsonParseError error;
|
||||
QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error);
|
||||
if(json.isObject() && json.object().contains("tag_name"))
|
||||
{
|
||||
QString tag = json.object().value("tag_name").toString();
|
||||
QString version = getVersion();
|
||||
if(version >= tag)
|
||||
QMessageBox::information(this, tr("Update check"), tr("You have newest version"));
|
||||
else
|
||||
{
|
||||
if(QMessageBox::question(this, tr("Update check"), tr("New version %1 is available. Do you want to download it now?").arg(tag)) == QMessageBox::Yes)
|
||||
{
|
||||
QUrl url(json.object().value("html_url").toString());
|
||||
qDebug() << url;
|
||||
if(url.host() == "gitea.nouspiro.space")
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::warning(this, tr("Update check"), tr("Failed to check version"));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::openFileManager()
|
||||
{
|
||||
#if !defined(FLATPAK) || !defined(__aarch64__)
|
||||
FileManager *filemanager = new FileManager(_openSuffix);
|
||||
connect(filemanager, &FileManager::openFile, this, static_cast<void (MainWindow::*)(const QString&)>(&MainWindow::loadFile));
|
||||
filemanager->show();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle()
|
||||
{
|
||||
ImagePtr ptr = m_ringList->currentImage();
|
||||
if(ptr)
|
||||
{
|
||||
QFileInfo info(ptr->name());
|
||||
QString title = info.fileName();
|
||||
QDir dir(m_ringList->currentDir());
|
||||
QString title = dir.relativeFilePath(ptr->name());
|
||||
if(ptr->info().num > 1)
|
||||
title += QString(" [%1/%2]").arg(ptr->info().index + 1).arg(ptr->info().num);
|
||||
if(m_database->isMarked(ptr->name()))
|
||||
title += " *";
|
||||
setWindowTitle(title);
|
||||
@@ -4,19 +4,18 @@
|
||||
#include <QMainWindow>
|
||||
#include <QSocketNotifier>
|
||||
#include "imageringlist.h"
|
||||
#include "imagescrollarea.h"
|
||||
#include "database.h"
|
||||
#include "imageinfo.h"
|
||||
#include "imagescrollareagl.h"
|
||||
#include "imagescrollarea.h"
|
||||
#include "filesystemwidget.h"
|
||||
#include "stretchtoolbar.h"
|
||||
#include "databaseview.h"
|
||||
#include "platesolving.h"
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
ImageScrollArea *m_image;
|
||||
ImageScrollAreaGL *m_imageGL;
|
||||
ImageRingList *m_ringList;
|
||||
StretchToolbar *m_stretchPanel;
|
||||
Database *m_database;
|
||||
@@ -24,30 +23,31 @@ class MainWindow : public QMainWindow
|
||||
FilesystemWidget *m_filesystem;
|
||||
Filetree *m_filetree;
|
||||
DataBaseView *m_databaseView;
|
||||
PlateSolving *_plateSolving = nullptr;
|
||||
static int socketPair[2];
|
||||
QSocketNotifier *socketNotifier;
|
||||
QString _lastDir;
|
||||
bool _maximized;
|
||||
QString _openFilter;
|
||||
QString _saveFilter;
|
||||
QSet<QString> _openSuffix;
|
||||
public:
|
||||
MainWindow(QWidget *parent = 0);
|
||||
~MainWindow() override;
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void keyReleaseEvent(QKeyEvent *event) override;
|
||||
void setupSigterm();
|
||||
static void signalHandler(int);
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void copyOrMove(bool copy);
|
||||
void copyOrMove(bool copy, const QString &dest);
|
||||
protected slots:
|
||||
public slots:
|
||||
void socketNotify();
|
||||
void updateWindowTitle();
|
||||
void pixmapLoaded(Image *image);
|
||||
void loadFile();
|
||||
void loadFile(const QString &path);
|
||||
void loadFiles(const QStringList &paths);
|
||||
void loadFile(int row);
|
||||
void loadDir();
|
||||
void indexDir();
|
||||
void indexDir(const QString &dir);
|
||||
void reindex();
|
||||
@@ -67,6 +67,8 @@ protected slots:
|
||||
void showMarkFilesDialog();
|
||||
void showSettingsDialog();
|
||||
void exportCSV();
|
||||
void checkNewVersion();
|
||||
void openFileManager();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
@@ -60,6 +60,6 @@ void MarkedFiles::clearSelected()
|
||||
void MarkedFiles::clearAll()
|
||||
{
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
db.exec("DELETE FROM files");
|
||||
QSqlQuery("DELETE FROM files", db);
|
||||
m_model->select();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef MTFPARAM_H
|
||||
#define MTFPARAM_H
|
||||
|
||||
struct MTFParam
|
||||
{
|
||||
float blackPoint[3] = {0.0f, 0.0f, 0.0f};
|
||||
float midPoint[3] = {0.5f, 0.5f, 0.5f};
|
||||
float whitePoint[3] = {1.0f, 1.0f, 1.0f};
|
||||
};
|
||||
|
||||
#endif // MTFPARAM_H
|
||||