diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt index 83c10c714..70034603e 100644 --- a/kstars/CMakeLists.txt +++ b/kstars/CMakeLists.txt @@ -1,1196 +1,1197 @@ add_subdirectory( data ) add_subdirectory( icons ) add_subdirectory( htmesh ) if (${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) SET(HAVE_KF5WIT 1) # if(NOT BUILD_KSTARS_LITE) # add_subdirectory( tools/whatsinteresting/qml) # endif(NOT BUILD_KSTARS_LITE) else() SET(HAVE_KF5WIT 0) endif() if (ANDROID AND CMAKE_TOOLCHAIN_FILE) include(${CMAKE_TOOLCHAIN_FILE}) endif () if (NOT ANDROID) find_package(ZLIB REQUIRED) find_package(Threads REQUIRED) endif () if(MSVC) add_definitions(-D_USE_MATH_DEFINES=1) add_definitions(-DNOMINMAX) endif() include_directories( ${kstars_SOURCE_DIR}/kstars ${kstars_SOURCE_DIR}/kstars/skyobjects ${kstars_SOURCE_DIR}/kstars/skycomponents ${kstars_SOURCE_DIR}/kstars/auxiliary ${kstars_SOURCE_DIR}/kstars/time ${kstars_SOURCE_DIR}/kstars/tools ) if (INDI_FOUND) if(BUILD_KSTARS_LITE) set (fits_klite_SRCS fitsviewer/fitsdata.cpp ) set (fits2_klite_SRCS fitsviewer/bayer.c fitsviewer/fpack.c fitsviewer/fpackutil.c ) include_directories(${CFITSIO_INCLUDE_DIR}) include_directories(${NOVA_INCLUDE_DIR}) set (indi_klite_SRCS indi/clientmanagerlite.cpp indi/inditelescopelite.cpp kstarslite/skyitems/skynodes/crosshairnode.cpp kstarslite/skyitems/telescopesymbolsitem.cpp ) endif () set(indiui_SRCS indi/streamform.ui indi/drivermanager.ui indi/opsindi.ui indi/indihostconf.ui indi/customdrivers.ui #indi/telescopewizard.ui ) set(indi_SRCS indi/drivermanager.cpp indi/servermanager.cpp indi/clientmanager.cpp indi/blobmanager.cpp indi/guimanager.cpp indi/driverinfo.cpp indi/deviceinfo.cpp indi/indidevice.cpp indi/indigroup.cpp indi/indiproperty.cpp indi/indielement.cpp indi/indistd.cpp indi/indilistener.cpp indi/inditelescope.cpp indi/indiccd.cpp indi/wsmedia.cpp indi/indifocuser.cpp indi/indifilter.cpp indi/indidome.cpp indi/indiweather.cpp indi/indicap.cpp indi/indilightbox.cpp indi/indidbus.cpp indi/opsindi.cpp #indi/telescopewizardprocess.cpp indi/streamwg.cpp indi/videowg.cpp indi/indiwebmanager.cpp indi/customdrivers.cpp ) if (CFITSIO_FOUND) set(ekosui_SRCS ekos/opsekos.ui ekos/manager.ui ekos/profileeditor.ui ekos/profilewizard.ui # Scheduler ekos/scheduler/scheduler.ui ekos/scheduler/mosaic.ui # Capture ekos/capture/capture.ui ekos/capture/calibrationoptions.ui ekos/capture/dslrinfo.ui ekos/capture/rotatorsettings.ui ekos/capture/customproperties.ui # Align ekos/align/align.ui ekos/align/opsastrometry.ui ekos/align/opsalign.ui ekos/align/opsastrometrycfg.ui ekos/align/opsastrometryindexfiles.ui ekos/align/mountmodel.ui ekos/align/opsastap.ui # Focus ekos/focus/focus.ui # Mount ekos/mount/mount.ui # Guide ekos/guide/guide.ui ekos/guide/opscalibration.ui ekos/guide/opsguide.ui ekos/guide/manualdither.ui ekos/observatory/observatory.ui #TODO remove from GIT #ekos/guide/guider.ui #ekos/guide/rcalibration.ui # Auxiliary ekos/auxiliary/filtersettings.ui ekos/auxiliary/opslogs.ui ekos/auxiliary/serialportassistant.ui # Ekos Live ekos/ekoslive/ekoslivedialog.ui ) set(ekos_SRCS ekos/ekos.cpp ekos/manager.cpp ekos/profileeditor.cpp ekos/profilewizard.cpp ekos/qMDNS.cpp ekos/opsekos.cpp # Auxiliary ekos/auxiliary/dome.cpp ekos/auxiliary/weather.cpp ekos/auxiliary/dustcap.cpp ekos/auxiliary/darklibrary.cpp ekos/auxiliary/filtermanager.cpp ekos/auxiliary/filterdelegate.cpp ekos/auxiliary/opslogs.cpp ekos/auxiliary/serialportassistant.cpp # Capture ekos/capture/capture.cpp ekos/capture/sequencejob.cpp ekos/capture/dslrinfodialog.cpp ekos/capture/rotatorsettings.cpp ekos/capture/customproperties.cpp # Scheduler ekos/scheduler/schedulerjob.cpp ekos/scheduler/scheduler.cpp ekos/scheduler/mosaic.cpp # Focus ekos/focus/focus.cpp # Mount ekos/mount/mount.cpp # Align ekos/align/align.cpp ekos/align/alignview.cpp ekos/align/astrometryparser.cpp ekos/align/opsastrometry.cpp ekos/align/opsalign.cpp ekos/align/opsastrometrycfg.cpp ekos/align/opsastrometryindexfiles.cpp ekos/align/opsastap.cpp ekos/align/offlineastrometryparser.cpp ekos/align/onlineastrometryparser.cpp ekos/align/remoteastrometryparser.cpp ekos/align/astapastrometryparser.cpp # Guide ekos/guide/guide.cpp ekos/guide/guideinterface.cpp ekos/guide/opscalibration.cpp ekos/guide/opsguide.cpp # Internal Guide ekos/guide/internalguide/gmath.cpp ekos/guide/internalguide/internalguider.cpp #ekos/guide/internalguide/guider.cpp ekos/guide/internalguide/matr.cpp #ekos/guide/internalguide/rcalibration.cpp ekos/guide/internalguide/vect.cpp ekos/guide/internalguide/imageautoguiding.cpp # External Guide ekos/guide/externalguide/phd2.cpp ekos/guide/externalguide/linguider.cpp #Observatory ekos/observatory/observatory.cpp ekos/observatory/observatorymodel.cpp ekos/observatory/observatorydomemodel.cpp ekos/observatory/observatoryweathermodel.cpp # Ekos Live ekos/ekoslive/ekosliveclient.cpp ekos/ekoslive/message.cpp ekos/ekoslive/media.cpp ekos/ekoslive/cloud.cpp ) endif(CFITSIO_FOUND) include_directories(${INDI_INCLUDE_DIR}) endif (INDI_FOUND) if (CFITSIO_FOUND) set (sep_SRCS fitsviewer/sep/analyse.c fitsviewer/sep/aperture.c fitsviewer/sep/background.c fitsviewer/sep/convolve.c fitsviewer/sep/deblend.c fitsviewer/sep/extract.c fitsviewer/sep/lutz.c fitsviewer/sep/util.c ) set (fits_SRCS fitsviewer/fitslabel.cpp fitsviewer/fitsviewer.cpp + fitsviewer/stretch.cpp fitsviewer/fitstab.cpp fitsviewer/fitsdebayer.cpp fitsviewer/opsfits.cpp ) if (Qt5DataVisualization_FOUND) set(fits_SRCS ${fits_SRCS} fitsviewer/starprofileviewer.cpp) endif() set (fits2_SRCS fitsviewer/bayer.c fitsviewer/fpack.c fitsviewer/fpackutil.c fitsviewer/fitshistogram.cpp fitsviewer/fitsview.cpp fitsviewer/fitsdata.cpp ) set (fitsui_SRCS fitsviewer/fitsheaderdialog.ui fitsviewer/statform.ui fitsviewer/fitsdebayer.ui indi/streamform.ui indi/recordingoptions.ui fitsviewer/fitshistogramui.ui fitsviewer/opsfits.ui ) include_directories(${CFITSIO_INCLUDE_DIR}) endif(CFITSIO_FOUND) IF (CFITSIO_FOUND) IF (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) IF (SANITIZERS) SET_SOURCE_FILES_PROPERTIES(fitsviewer/bayer.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitsdata.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitshistogram.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitsview.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") ELSE () SET_SOURCE_FILES_PROPERTIES(fitsviewer/bayer.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") ENDIF () SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/analyse.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/aperture.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -Wno-pointer-arith") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/background.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/deblend.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -Wno-incompatible-pointer-types-discards-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/extract.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/lutz.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/util.c PROPERTIES COMPILE_FLAGS "-Wno-incompatible-pointer-types-discards-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpack.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpackutil.c PROPERTIES COMPILE_FLAGS "-Wno-error") ELSEIF (NOT WIN32) SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpack.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpackutil.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/aperture.c PROPERTIES COMPILE_FLAGS "-Wno-pointer-arith") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/deblend.c PROPERTIES COMPILE_FLAGS "-Wno-discarded-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/util.c PROPERTIES COMPILE_FLAGS "-Wno-discarded-qualifiers") ENDIF () ENDIF () if(WCSLIB_FOUND) include_directories( ${WCSLIB_INCLUDE_DIR} ) endif(WCSLIB_FOUND) set(xplanet_SRCS xplanet/opsxplanet.cpp ) set(xplanetui_SRCS xplanet/opsxplanet.ui ) ########### next target ############### set(libkstarstools_SRCS tools/altvstime.cpp tools/avtplotwidget.cpp tools/calendarwidget.cpp tools/conjunctions.cpp tools/eclipsetool.cpp tools/eclipsehandler.cpp tools/eclipsetool/lunareclipsehandler.cpp # tools/jmoontool.cpp tools/approachsolver.cpp tools/ksconjunct.cpp tools/eqplotwidget.cpp tools/astrocalc.cpp tools/modcalcangdist.cpp tools/modcalcapcoord.cpp tools/modcalcaltaz.cpp tools/modcalcdaylength.cpp tools/modcalceclipticcoords.cpp tools/modcalcvizequinox.cpp tools/modcalcgalcoord.cpp tools/modcalcgeodcoord.cpp tools/modcalcjd.cpp tools/modcalcplanets.cpp tools/modcalcsidtime.cpp tools/modcalcvlsr.cpp tools/observinglist.cpp tools/obslistpopupmenu.cpp tools/sessionsortfilterproxymodel.cpp tools/obslistwizard.cpp tools/planetviewer.cpp tools/pvplotwidget.cpp tools/scriptargwidgets.cpp tools/scriptbuilder.cpp tools/scriptfunction.cpp tools/skycalendar.cpp tools/wutdialog.cpp tools/flagmanager.cpp tools/horizonmanager.cpp tools/nameresolver.cpp tools/polarishourangle.cpp #FIXME Port to KF5 #tools/moonphasetool.cpp tools/starhopper.cpp tools/eyepiecefield.cpp tools/exporteyepieceview.cpp tools/starhopperdialog.cpp tools/adddeepskyobject.cpp ) if(${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) set(libkstarstools_SRCS ${libkstarstools_SRCS} tools/whatsinteresting/skyobjlistmodel.cpp tools/whatsinteresting/wiview.cpp tools/whatsinteresting/modelmanager.cpp tools/whatsinteresting/skyobjitem.cpp tools/whatsinteresting/wilpsettings.cpp tools/whatsinteresting/wiequipsettings.cpp tools/whatsinteresting/obsconditions.cpp tools/whatsinteresting/skyobjdescription.cpp ) endif() ki18n_wrap_ui(libkstarstools_ui_SRCS tools/altvstime.ui tools/argchangeviewoption.ui tools/argexportimage.ui tools/argloadcolorscheme.ui tools/arglooktoward.ui tools/argfindobject.ui tools/argprintimage.ui tools/argsetaltaz.ui tools/argsetcolor.ui tools/argsetgeolocation.ui tools/argsetlocaltime.ui tools/argsetradec.ui tools/argsettrack.ui tools/argtimescale.ui tools/argwaitfor.ui tools/argwaitforkey.ui tools/argzoom.ui tools/conjunctions.ui tools/eclipsetool.ui tools/modcalcangdist.ui tools/modcalcapcoord.ui tools/modcalcaltaz.ui tools/modcalcdaylength.ui tools/modcalceclipticcoords.ui tools/modcalcvizequinox.ui tools/modcalcgalcoord.ui tools/modcalcgeod.ui tools/modcalcjd.ui tools/modcalcplanets.ui tools/modcalcsidtime.ui tools/modcalcvlsr.ui tools/observinglist.ui tools/obslistwizard.ui tools/optionstreeview.ui tools/planetviewer.ui tools/scriptbuilder.ui tools/scriptnamedialog.ui tools/skycalendar.ui tools/wutdialog.ui tools/flagmanager.ui tools/starhopperdialog.ui tools/horizonmanager.ui tools/adddeepskyobject.ui tools/polarishourangle.ui ) if (${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) ki18n_wrap_ui(libkstarstools_ui_SRCS tools/whatsinteresting/wilpsettings.ui tools/whatsinteresting/wiequipsettings.ui ) endif() set(libkstarswidgets_SRCS widgets/clicklabel.cpp widgets/dmsbox.cpp widgets/draglistbox.cpp widgets/fovwidget.cpp widgets/logedit.cpp widgets/magnitudespinbox.cpp widgets/mapcanvas.cpp widgets/thumbimage.cpp widgets/timespinbox.cpp widgets/timestepbox.cpp widgets/timeunitbox.cpp widgets/infoboxwidget.cpp # widgets/genericcalendarwidget.cpp # widgets/moonphasecalendarwidget.cpp widgets/kshelplabel.cpp widgets/unitspinboxwidget.cpp ) ki18n_wrap_ui(libkstarswidgets_ui_SRCS # widgets/genericcalendarwidget.ui widgets/unitspinboxwidget.ui ) set(kstars_KCFG_SRCS Options.kcfgc) set(kstars_options_SRCS options/opsadvanced.cpp options/opscatalog.cpp options/opscolors.cpp options/opsguides.cpp options/opssolarsystem.cpp options/opssatellites.cpp options/opssupernovae.cpp ) set(kstars_optionsui_SRCS options/opsadvanced.ui options/opscatalog.ui options/opscolors.ui options/opsguides.ui options/opssolarsystem.ui options/opssatellites.ui options/opssupernovae.ui ) set(kstars_dialogs_SRCS dialogs/addcatdialog.cpp dialogs/addlinkdialog.cpp dialogs/detaildialog.cpp dialogs/finddialog.cpp dialogs/focusdialog.cpp dialogs/fovdialog.cpp dialogs/locationdialog.cpp dialogs/timedialog.cpp dialogs/exportimagedialog.cpp ) set(kstars_dialogsui_SRCS dialogs/addcatdialog.ui dialogs/addlinkdialog.ui dialogs/details_database.ui dialogs/details_data.ui dialogs/details_data_comet.ui dialogs/details_links.ui dialogs/details_log.ui dialogs/details_position.ui dialogs/finddialog.ui dialogs/focusdialog.ui dialogs/fovdialog.ui dialogs/locationdialog.ui dialogs/wizwelcome.ui dialogs/wizlocation.ui dialogs/wizdownload.ui dialogs/wizdata.ui dialogs/newfov.ui dialogs/exportimagedialog.ui ) set(hips_SRCS hips/healpix.cpp hips/hipsrenderer.cpp hips/scanrender.cpp hips/pixcache.cpp hips/urlfiledownload.cpp hips/opships.cpp ) set(hips_manager_SRCS hips/hipsmanager.cpp ) set(oal_SRCS oal/log.cpp oal/observer.cpp oal/site.cpp oal/session.cpp oal/scope.cpp oal/eyepiece.cpp oal/filter.cpp oal/observation.cpp oal/lens.cpp oal/equipmentwriter.cpp oal/observeradd.cpp oal/execute.cpp ) set(printing_SRCS printing/detailstable.cpp printing/finderchart.cpp printing/foveditordialog.cpp printing/fovsnapshot.cpp printing/kstarsdocument.cpp printing/legend.cpp printing/loggingform.cpp printing/printingwizard.cpp printing/pwizchartconfig.cpp printing/pwizchartcontents.cpp printing/pwizfovbrowse.cpp printing/pwizfovconfig.cpp printing/pwizfovmanual.cpp printing/pwizfovsh.cpp printing/pwizfovtypeselection.cpp printing/pwizobjectselection.cpp printing/pwizprint.cpp printing/shfovexporter.cpp printing/simplefovexporter.cpp ) set(printingui_SRCS printing/foveditordialog.ui printing/pwizchartconfig.ui printing/pwizchartcontents.ui printing/pwizfovbrowse.ui printing/pwizfovconfig.ui printing/pwizfovmanual.ui printing/pwizfovsh.ui printing/pwizfovtypeselection.ui printing/pwizobjectselection.ui printing/pwizprint.ui printing/pwizwelcome.ui ) set( kstars_KCFG_SRCS Options.kcfgc ) set(libkstarscomponents_SRCS skycomponents/skylabeler.cpp skycomponents/highpmstarlist.cpp skycomponents/skymapcomposite.cpp skycomponents/skymesh.cpp skycomponents/linelistindex.cpp skycomponents/linelistlabel.cpp skycomponents/noprecessindex.cpp skycomponents/listcomponent.cpp skycomponents/pointlistcomponent.cpp skycomponents/solarsystemsinglecomponent.cpp skycomponents/solarsystemlistcomponent.cpp skycomponents/earthshadowcomponent.cpp skycomponents/asteroidscomponent.cpp skycomponents/cometscomponent.cpp skycomponents/planetmoonscomponent.cpp skycomponents/solarsystemcomposite.cpp skycomponents/satellitescomponent.cpp skycomponents/starcomponent.cpp skycomponents/deepstarcomponent.cpp skycomponents/deepskycomponent.cpp skycomponents/catalogcomponent.cpp skycomponents/syncedcatalogcomponent.cpp skycomponents/constellationartcomponent.cpp skycomponents/constellationboundarylines.cpp skycomponents/constellationlines.cpp skycomponents/constellationnamescomponent.cpp skycomponents/supernovaecomponent.cpp skycomponents/coordinategrid.cpp skycomponents/equatorialcoordinategrid.cpp skycomponents/horizontalcoordinategrid.cpp skycomponents/localmeridiancomponent.cpp skycomponents/ecliptic.cpp skycomponents/equator.cpp skycomponents/artificialhorizoncomponent.cpp skycomponents/hipscomponent.cpp skycomponents/horizoncomponent.cpp skycomponents/milkyway.cpp skycomponents/skycomponent.cpp skycomponents/skycomposite.cpp skycomponents/starblock.cpp skycomponents/starblocklist.cpp skycomponents/starblockfactory.cpp skycomponents/culturelist.cpp skycomponents/flagcomponent.cpp skycomponents/targetlistcomponent.cpp ) #LIST(APPEND libkstarscomponents_SRCS # #skycomponents/notifyupdatesui.cpp # ) IF (BUILD_KSTARS_LITE) set(libkstarstools_ui_klite_SRCS tools/nameresolver.cpp ) ENDIF () set(kstars_skyobjects_SRCS skyobjects/constellationsart.cpp skyobjects/deepskyobject.cpp # skyobjects/jupitermoons.cpp skyobjects/planetmoons.cpp skyobjects/ksasteroid.cpp skyobjects/kscomet.cpp skyobjects/ksmoon.cpp skyobjects/ksearthshadow.cpp skyobjects/ksplanetbase.cpp skyobjects/ksplanet.cpp #skyobjects/kspluto.cpp skyobjects/kssun.cpp skyobjects/skyline.cpp skyobjects/skyobject.cpp skyobjects/skypoint.cpp skyobjects/starobject.cpp skyobjects/trailobject.cpp skyobjects/satellite.cpp skyobjects/satellitegroup.cpp skyobjects/supernova.cpp ) set(kstars_projection_SRCS projections/projector.cpp projections/lambertprojector.cpp projections/gnomonicprojector.cpp projections/stereographicprojector.cpp projections/orthographicprojector.cpp projections/azimuthalequidistantprojector.cpp projections/equirectangularprojector.cpp ) set(kstars_extra_SRCS auxiliary/colorscheme.cpp auxiliary/dms.cpp auxiliary/cachingdms.cpp auxiliary/geolocation.cpp auxiliary/ksfilereader.cpp auxiliary/ksuserdb.cpp auxiliary/binfilehelper.cpp auxiliary/ksutils.cpp auxiliary/ksdssimage.cpp auxiliary/ksdssdownloader.cpp auxiliary/nonlineardoublespinbox.cpp auxiliary/profileinfo.cpp auxiliary/filedownloader.cpp auxiliary/kspaths.cpp auxiliary/QRoundProgressBar.cpp auxiliary/skyobjectlistmodel.cpp auxiliary/ksnotification.cpp auxiliary/ksmessagebox.cpp auxiliary/QProgressIndicator.cpp auxiliary/ctkrangeslider.cpp time/simclock.cpp time/kstarsdatetime.cpp time/timezonerule.cpp ksnumbers.cpp kstarsdata.cpp texturemanager.cpp #to minimize number of indef KSTARS_LITE skypainter.cpp ) SET(kstars_extra_kstars_SRCS auxiliary/thememanager.cpp auxiliary/schememanager.cpp auxiliary/imageviewer.cpp auxiliary/xplanetimageviewer.cpp auxiliary/fov.cpp auxiliary/thumbnailpicker.cpp auxiliary/thumbnaileditor.cpp auxiliary/imageexporter.cpp auxiliary/kswizard.cpp auxiliary/qcustomplot.cpp kstarsdbus.cpp kspopupmenu.cpp ksalmanac.cpp kstarsactions.cpp kstarsinit.cpp kstars.cpp kstarssplash.cpp skymap.cpp skymapdrawabstract.cpp skymapqdraw.cpp skymapevents.cpp skyqpainter.cpp ) # Temporary solution to allow use of qml files from source dir DELETE SET(KSTARSLITE_CPP_OPTIONS -DSOURCE_DIR=\"${kstars_SOURCE_DIR}\" -DQML_IMPORT="${CMAKE_CURRENT_SOURCE_DIR}") set(klite_SRCS kstarslite.cpp kstarsliteinit.cpp skymaplite.cpp skymapliteevents.cpp #Wrappers kstarslite/skypointlite.cpp kstarslite/skyobjectlite.cpp #ImageProvider kstarslite/imageprovider.cpp #Dialogs kstarslite/dialogs/detaildialoglite.cpp kstarslite/dialogs/finddialoglite.cpp kstarslite/dialogs/locationdialoglite.cpp #RootNode kstarslite/skyitems/rootnode.cpp kstarslite/skyitems/skyopacitynode.cpp kstarslite/skyitems/typedeflite.h #SkyItems kstarslite/skyitems/skyitem.cpp kstarslite/skyitems/planetsitem.cpp kstarslite/skyitems/asteroidsitem.cpp kstarslite/skyitems/cometsitem.cpp kstarslite/skyitems/horizonitem.cpp kstarslite/skyitems/labelsitem.cpp kstarslite/skyitems/constellationnamesitem.cpp kstarslite/skyitems/staritem.cpp kstarslite/skyitems/deepstaritem.cpp kstarslite/skyitems/deepskyitem.cpp kstarslite/skyitems/constellationartitem.cpp kstarslite/skyitems/satellitesitem.cpp kstarslite/skyitems/supernovaeitem.cpp kstarslite/skyitems/fovitem.cpp kstarslite/skyitems/syncedcatalogitem.cpp #Line kstarslite/skyitems/lines/linesitem.cpp kstarslite/skyitems/lines/equatoritem.cpp kstarslite/skyitems/lines/eclipticitem.cpp kstarslite/skyitems/lines/milkywayitem.cpp #SkyNodes kstarslite/skyitems/skynodes/planetnode.cpp kstarslite/skyitems/skynodes/skynode.cpp kstarslite/skyitems/skynodes/pointsourcenode.cpp kstarslite/skyitems/skynodes/planetmoonsnode.cpp kstarslite/skyitems/skynodes/horizonnode.cpp kstarslite/skyitems/skynodes/labelnode.cpp kstarslite/skyitems/skynodes/guidelabelnode.cpp kstarslite/skyitems/skynodes/deepskynode.cpp kstarslite/skyitems/skynodes/dsosymbolnode.cpp kstarslite/skyitems/skynodes/skypolygonnode.cpp kstarslite/skyitems/skynodes/constellationartnode.cpp kstarslite/skyitems/skynodes/satellitenode.cpp kstarslite/skyitems/skynodes/supernovanode.cpp kstarslite/skyitems/skynodes/trixelnode.cpp kstarslite/skyitems/skynodes/fovsymbolnode.cpp #Nodes kstarslite/skyitems/skynodes/nodes/pointnode.cpp kstarslite/skyitems/skynodes/nodes/polynode.cpp kstarslite/skyitems/skynodes/nodes/linenode.cpp kstarslite/skyitems/skynodes/nodes/ellipsenode.cpp kstarslite/skyitems/skynodes/nodes/rectnode.cpp #Other kstarslite/deviceorientation.cpp ) set(kstarslite_libtess_SRC #libtess libtess/gluos.h libtess/priorityq-sort.h libtess/sweep.c libtess/tessmono.c libtess/dict-list.h libtess/glu.h libtess/tessellate.c libtess/dict.c libtess/geom.c libtess/memalloc.c libtess/mesh.c libtess/normal.c libtess/priorityq.c libtess/priorityq-heap.c libtess/render.c libtess/tess.c ) IF (BUILD_KSTARS_LITE) ADD_CUSTOM_TARGET(convert_translations ${CMAKE_SOURCE_DIR}/tools/convert_translations.sh ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) ADD_DEPENDENCIES(convert_translations fetch-translations) IF (ANDROID) ADD_CUSTOM_TARGET(convert_translations_to_android ${CMAKE_SOURCE_DIR}/tools/convert_translations.sh ${CMAKE_BINARY_DIR}/packaging/android/export/share/kstars WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) ADD_DEPENDENCIES(convert_translations_to_android fetch-translations) ENDIF () ENDIF () IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") SET_SOURCE_FILES_PROPERTIES(${kstarslite_libtess_SRC} PROPERTIES COMPILE_FLAGS "-Wno-error") ENDIF () #Qml files will be probably moved to user's data dir, but for use #with QtCreator it is more convenient to have them here set(kstarsliteqml_SRCS kstarslite/qml/main.qml kstarslite/qml/constants/Constants.qml kstarslite/qml/modules/SkyMapLiteWrapper.qml kstarslite/qml/modules/BottomMenu.qml kstarslite/qml/modules/KSPage.qml kstarslite/qml/modules/KSListView.qml kstarslite/qml/modules/KSLabel.qml kstarslite/qml/modules/KSText.qml kstarslite/qml/modules/KSTabButton.qml kstarslite/qml/modules/KSTab.qml kstarslite/qml/modules/KSTabBarArrow.qml kstarslite/qml/modules/KSTextField.qml kstarslite/qml/modules/KSButton.qml kstarslite/qml/modules/TopMenu.qml kstarslite/qml/modules/helpers/TopMenuButton.qml kstarslite/qml/modules/helpers/BottomMenuButton.qml kstarslite/qml/modules/Splash.qml kstarslite/qml/modules/helpers/TimeSpinBox.qml kstarslite/qml/modules/TimePage.qml #Popups kstarslite/qml/modules/popups/ProjectionsPopup.qml kstarslite/qml/modules/popups/FOVPopup.qml kstarslite/qml/modules/popups/ColorSchemePopup.qml #Menus kstarslite/qml/modules/menus/ContextMenu.qml #Helpers kstarslite/qml/modules/helpers/PassiveNotification.qml kstarslite/qml/modules/helpers/KSMenuItem.qml kstarslite/qml/modules/helpers/TelescopeControl.qml #Dialogs kstarslite/qml/dialogs/FindDialog.qml kstarslite/qml/dialogs/LocationDialog.qml kstarslite/qml/dialogs/DetailsDialog.qml kstarslite/qml/dialogs/AboutDialog.qml kstarslite/qml/dialogs/helpers/DetailsItem.qml kstarslite/qml/dialogs/helpers/DetailsAddLink.qml kstarslite/qml/dialogs/helpers/LocationEdit.qml kstarslite/qml/dialogs/helpers/LocationLoading.qml kstarslite/qml/dialogs/menus/DetailsLinkMenu.qml kstarslite/qml/dialogs/menus/LocationsGeoMenu.qml #INDI kstarslite/qml/indi/INDIControlPanel.qml kstarslite/qml/indi/DevicePanel.qml kstarslite/qml/indi/ImagePreview.qml kstarslite/qml/indi/modules/MotionControl.qml kstarslite/qml/indi/modules/Led.qml kstarslite/qml/indi/modules/KSLed.qml kstarslite/qml/indi/modules/Property.qml kstarslite/qml/indi/modules/KSComboBox.qml kstarslite/qml/indi/modules/KSButtonSwitch.qml kstarslite/qml/indi/modules/KSCheckBox.qml kstarslite/qml/indi/modules/KSINDIText.qml kstarslite/qml/indi/modules/KSINDITextField.qml kstarslite/qml/indi/modules/KSButtonsSwitchRow.qml #Tutorial kstarslite/qml/modules/tutorial/TutorialPopup.qml kstarslite/qml/modules/tutorial/TutorialExitPopup.qml kstarslite/qml/modules/tutorial/TutorialStep1.qml kstarslite/qml/modules/tutorial/TutorialStep2.qml kstarslite/qml/modules/tutorial/TutorialStep3.qml kstarslite/qml/modules/tutorial/TutorialStep4.qml kstarslite/qml/modules/tutorial/TutorialStep5.qml kstarslite/qml/modules/tutorial/TutorialPane.qml ) add_subdirectory(kstarslite/qml) ADD_CUSTOM_TARGET(kstarsliteqml SOURCES ${kstarsliteqml_SRCS}) if(ANDROID) add_subdirectory(kstarslite/res) endif(ANDROID) set(kstars_SRCS ${indi_SRCS} ${fits_SRCS} ${ekos_SRCS} ${libkstarswidgets_SRCS} ${libkstarscomponents_SRCS} ${libkstarstools_SRCS} ${kstars_extra_SRCS} ${kstars_extra_kstars_SRCS} ${kstars_projection_SRCS} ${xplanet_SRCS} ${kstars_options_SRCS} ${kstars_skyobjects_SRCS} ${kstars_dialogs_SRCS} ${hips_SRCS} ${oal_SRCS} ${printing_SRCS} #KStars Lite ${kstarslite_SRCS} # Generated files ${libkstarstools_ui_SRCS} ${libkstarswidgets_ui_SRCS} ) set(kstarslite_SRCS ${indi_klite_SRCS} ${libkstarscomponents_SRCS} ${kstars_extra_SRCS} ${kstars_projection_SRCS} ${kstars_skyobjects_SRCS} # KStars Lite sources ${klite_SRCS} # Generated files ${libkstarstools_ui_klite_SRCS} ) # Generate all the necessary QLoggingCategory files ecm_qt_declare_logging_category(kstars_SRCS HEADER kstars_debug.h IDENTIFIER KSTARS CATEGORY_NAME org.kde.kstars) ecm_qt_declare_logging_category(kstars_SRCS HEADER indi_debug.h IDENTIFIER KSTARS_INDI CATEGORY_NAME org.kde.kstars.indi) ecm_qt_declare_logging_category(kstars_SRCS HEADER fits_debug.h IDENTIFIER KSTARS_FITS CATEGORY_NAME org.kde.kstars.fits) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_debug.h IDENTIFIER KSTARS_EKOS CATEGORY_NAME org.kde.kstars.ekos) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_capture_debug.h IDENTIFIER KSTARS_EKOS_CAPTURE CATEGORY_NAME org.kde.kstars.ekos.capture) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_focus_debug.h IDENTIFIER KSTARS_EKOS_FOCUS CATEGORY_NAME org.kde.kstars.ekos.focus) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_align_debug.h IDENTIFIER KSTARS_EKOS_ALIGN CATEGORY_NAME org.kde.kstars.ekos.align) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_guide_debug.h IDENTIFIER KSTARS_EKOS_GUIDE CATEGORY_NAME org.kde.kstars.ekos.guide) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_mount_debug.h IDENTIFIER KSTARS_EKOS_MOUNT CATEGORY_NAME org.kde.kstars.ekos.mount) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_scheduler_debug.h IDENTIFIER KSTARS_EKOS_SCHEDULER CATEGORY_NAME org.kde.kstars.ekos.scheduler) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_observatory_debug.h IDENTIFIER KSTARS_EKOS_OBSERVATORY CATEGORY_NAME org.kde.kstars.ekos.observatory) kconfig_add_kcfg_files(kstars_SRCS ${kstars_KCFG_SRCS}) ecm_qt_declare_logging_category(kstarslite_SRCS HEADER kstars_debug.h IDENTIFIER KSTARS CATEGORY_NAME org.kde.kstars) ecm_qt_declare_logging_category(kstarslite_SRCS HEADER fits_debug.h IDENTIFIER KSTARS_FITS CATEGORY_NAME org.kde.kstars.fits) kconfig_add_kcfg_files(kstarslite_SRCS ${kstars_KCFG_SRCS}) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(kstars kstars_SRCS 10 cpp) ENABLE_UNITY_BUILD(kstarslite kstarslite_SRCS 10 cpp) ENDIF () set(kstars_SRCS ${kstars_SRCS} ${fits2_SRCS} ${sep_SRCS} ${hips_manager_SRCS}) set(kstarslite_SRCS ${kstarslite_SRCS} ${fits_klite_SRCS} ${sep_SRCS} ${fits2_klite_SRCS} ${kstarslite_libtess_SRC}) IF (NOT ANDROID) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.xml kstars.h KStars) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.SimClock.xml simclock.h SimClock) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.FOV.xml fov.h FOV) IF (INDI_FOUND) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.INDI.xml indi/indidbus.h INDIDBus) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.xml ekos/manager.h Ekos::Manager) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Capture.xml ekos/capture/capture.h Ekos::Capture) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Focus.xml ekos/focus/focus.h Ekos::Focus) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Guide.xml ekos/guide/guide.h Ekos::Guide) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Align.xml ekos/align/align.h Ekos::Align) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Mount.xml ekos/mount/mount.h Ekos::Mount) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Dome.xml ekos/auxiliary/dome.h Ekos::Dome) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Weather.xml ekos/auxiliary/weather.h Ekos::Weather) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.DustCap.xml ekos/auxiliary/dustcap.h Ekos::DustCap) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Scheduler.xml ekos/scheduler/scheduler.h Ekos::Scheduler) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Observatory.xml ekos/observatory/observatory.h Ekos::Observatory) ENDIF () ki18n_wrap_ui(kstars_SRCS ${indiui_SRCS} ${ui_SRCS} ${fitsui_SRCS} ${ekosui_SRCS} ${xplanetui_SRCS} ${kstars_optionsui_SRCS} ${kstars_dialogsui_SRCS} ${printingui_SRCS} auxiliary/thumbnailpicker.ui auxiliary/thumbnaileditor.ui oal/observeradd.ui oal/equipmentwriter.ui oal/execute.ui hips/opships.ui hips/opshipsdisplay.ui hips/opshipscache.ui #skycomponents/notifyupdatesui.ui ) add_library(KStarsLib STATIC ${kstars_SRCS}) include(GenerateExportHeader) generate_export_header(KStarsLib) target_link_libraries(KStarsLib LibKSDataHandlers htmesh KF5::Crash KF5::I18n KF5::NewStuff KF5::KIOFileWidgets KF5::WidgetsAddons KF5::Plotting KF5::Notifications Qt5::Gui Qt5::PrintSupport Qt5::Sql Qt5::Svg Qt5::Qml Qt5::Quick Qt5::Network #Qt5::Positioning Qt5::Concurrent Qt5::WebSockets ${ZLIB_LIBRARIES} ) if (Qt5Keychain_FOUND) include_directories(KStarsLib PUBLIC ${QTKEYCHAIN_INCLUDE_DIRS}) target_link_libraries(KStarsLib ${QTKEYCHAIN_LIBRARIES}) endif(Qt5Keychain_FOUND) if (Qt5DataVisualization_FOUND) target_link_libraries(KStarsLib Qt5::DataVisualization) endif(Qt5DataVisualization_FOUND) if (KF5NotifyConfig_FOUND) target_link_libraries(KStarsLib KF5::NotifyConfig) endif(KF5NotifyConfig_FOUND) if(NOT WIN32) target_link_libraries(KStarsLib m) endif(NOT WIN32) ENDIF () if (BUILD_KSTARS_LITE) add_library(KStarsLiteLib STATIC ${kstarslite_SRCS}) target_link_libraries(KStarsLiteLib LibKSDataHandlers htmesh KF5::I18n KF5::Plotting KF5::ConfigGui Qt5::Gui Qt5::Sql Qt5::Qml Qt5::Quick Qt5::QuickControls2 Qt5::Positioning Qt5::PositioningQuick Qt5::Concurrent ${ZLIB_LIBRARIES} ) if (ANDROID) target_link_libraries(KStarsLiteLib Qt5::AndroidExtras) endif () endif () if (CFITSIO_FOUND) if (NOT ANDROID) target_include_directories(KStarsLib PUBLIC ${CFITSIO_INCLUDE_DIR}) target_link_libraries(KStarsLib ${CFITSIO_LIBRARIES}) endif() if (BUILD_KSTARS_LITE) target_include_directories(KStarsLiteLib PUBLIC ${CFITSIO_INCLUDE_DIR}) target_link_libraries(KStarsLiteLib ${CFITSIO_LIBRARIES}) endif() endif(CFITSIO_FOUND) if(INDI_FOUND) if (NOT ANDROID) find_package(Nova REQUIRED) include_directories(${NOVA_INCLUDE_DIR}) endif () ## Support Multiple Platforms. All Require INDI ## WIN32 Desktop: Requires INDI Qt5 Client + GSL ## WIN32 Lite: Requires INDI Qt5 Client ## Linux + MacOS Desktop: Requires INDI Client + GSL ## Linux + MacOS Lite: Requires INDI Qt5 Client ## Android: Requires INDI Qt5 Client built for Android if (NOT ANDROID) find_package(GSL REQUIRED) include_directories(${GSL_INCLUDE_DIRS}) target_link_libraries(KStarsLib ${GSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} KF5::Notifications) endif () if(WIN32 OR ANDROID) if(ANDROID) target_link_libraries(KStarsLiteLib ${INDI_CLIENT_ANDROID_LIBRARIES} ${CFITSIO_LIBRARIES} ${LIBRAW_LIBRARIES}) target_compile_options(KStarsLiteLib PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) else(ANDROID) target_link_libraries(KStarsLib ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES}) endif(ANDROID) else(WIN32 OR ANDROID) if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${INDI_CLIENT_QT_LIBRARIES} ${NOVA_LIBRARIES} z) target_compile_options(KStarsLiteLib PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) endif(BUILD_KSTARS_LITE) target_link_libraries(KStarsLib ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES} z) endif(WIN32 OR ANDROID) endif(INDI_FOUND) if(WCSLIB_FOUND) target_link_libraries(KStarsLib ${WCSLIB_LIBRARIES}) if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${WCSLIB_LIBRARIES}) endif() endif (WCSLIB_FOUND) if(LibRaw_FOUND) if (NOT ANDROID) target_link_libraries(KStarsLib ${LibRaw_LIBRARIES}) endif() if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${LibRaw_LIBRARIES}) endif() endif (LibRaw_FOUND) #FIXME Enable OpenGL Later #if( OPENGL_FOUND ) # target_link_libraries(KStarsLib # ${OPENGL_LIBRARIES} # ${QT_QTOPENGL_LIBRARY} # ) #endif( OPENGL_FOUND ) set (KSTARS_APP_SRCS main.cpp ) # add icon to application sources ecm_add_app_icon(KSTARS_APP_SRCS ICONS ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kstars.png ) qt5_add_resources(KSTARS_APP_SRCS data/kstars.qrc) if (ANDROID) add_library(kstars SHARED ${KSTARS_APP_SRCS}) target_compile_options(kstars PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) add_dependencies(KStarsLiteLib cfitsio indi raw) target_link_libraries(kstars KStarsLiteLib) else () if (BUILD_KSTARS_LITE) add_executable(kstars_lite ${KSTARS_APP_SRCS}) target_compile_options(kstars_lite PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) target_link_libraries(kstars_lite KStarsLiteLib) endif() add_executable(kstars ${KSTARS_APP_SRCS}) target_link_libraries(kstars KStarsLib) endif () install(TARGETS kstars ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install(PROGRAMS org.kde.kstars.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kstars.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES kstars.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) if(INDI_FOUND) #install(FILES ekos/mount/mountbox.qml DESTINATION ${KDE_INSTALL_DATADIR}/kstars/ekos/mount/qml) #install(DIRECTORY ekos/mount/ DESTINATION ${KDE_INSTALL_DATADIR}/kstars/ekos/mount/qml # FILES_MATCHING PATTERN "*.png") endif() if (NOT ANDROID AND BUILD_KSTARS_LITE) install(TARGETS kstars_lite ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) endif() diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index 5a26b61ec..6bc848fc0 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1771 +1,1718 @@ /* FITS View Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "config-kstars.h" #include "fitsview.h" #include "fitsdata.h" #include "fitslabel.h" #include "kspopupmenu.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymap.h" #include "fits_debug.h" +#include "stretch.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include #include #include #include #include #include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 +namespace +{ + +void doStretch(FITSData *data, QImage *outputImage, bool stretchOn) +{ + if (outputImage->isNull()) + return; + Stretch stretch(static_cast(data->width()), + static_cast(data->height()), + data->channels(), data->property("dataType").toInt()); + if (stretchOn) + stretch.setParams(stretch.computeParams(data->getImageBuffer())); + stretch.run(data->getImageBuffer(), outputImage); +} + +} // namespace + + FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { + stretchImage = Options::autoStretch(); + grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); filter = filterType; mode = fitsMode; setBackgroundRole(QPalette::Dark); markerCrosshair.setX(0); markerCrosshair.setY(0); setBaseSize(740, 530); connect(image_frame.get(), SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar))); connect(image_frame.get(), SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int))); connect(image_frame.get(), SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int))); connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState())); connect(&fitsWatcher, &QFutureWatcher::finished, this, &FITSView::loadInFrame); image_frame->setMouseTracking(true); setCursorMode( selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode noImageLabel = new QLabel(); noImage.load(":/images/noimage.png"); noImageLabel->setPixmap(noImage); noImageLabel->setAlignment(Qt::AlignCenter); this->setWidget(noImageLabel); redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); } FITSView::~FITSView() { fitsWatcher.waitForFinished(); wcsWatcher.waitForFinished(); delete (imageData); } /** This method looks at what mouse mode is currently selected and updates the cursor to match. */ void FITSView::updateMouseCursor() { if (cursorMode == dragCursor) { if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) { if (!image_frame->getMouseButtonDown()) viewport()->setCursor(Qt::PointingHandCursor); else viewport()->setCursor(Qt::ClosedHandCursor); } else viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == selectCursor) { viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == scopeCursor) { viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); } else if (cursorMode == crosshairCursor) { viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); } } /** This is how the mouse mode gets set. The default for a FITSView in a FITSViewer should be the dragMouse The default for a FITSView in the Focus or Align module should be the selectMouse The different defaults are accomplished by putting making the actual default mouseMode the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse. */ void FITSView::setCursorMode(CursorMode mode) { cursorMode = mode; updateMouseCursor(); if (mode == scopeCursor && imageHasWCS()) { if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } } } void FITSView::resizeEvent(QResizeEvent * event) { if ((imageData == nullptr) && noImageLabel != nullptr) { noImageLabel->setPixmap( noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); noImageLabel->setFixedSize(width() - 5, height() - 5); } QScrollArea::resizeEvent(event); } void FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case image is still loading, wait until it is done. fitsWatcher.waitForFinished(); // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } bool FITSView::loadFITSFromData(FITSData *data, const QString &inFilename) { if (imageData != nullptr) { delete imageData; imageData = nullptr; } if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); // Takes control of the objects passed in. imageData = data; return processData(); } bool FITSView::processData() { // Set current width and height currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image initDisplayImage(); imageData->applyFilter(filter); // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); return false; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); return false; } } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } else syncWCSState(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); if (showStarProfile) { if(floatingToolBar != nullptr) toggleProfileAction->setChecked(true); //Need to wait till the Focus module finds stars, if its the Focus module. QTimer::singleShot(100, this, SLOT(viewStarProfile())); } scaledImage = QImage(); updateFrame(); return true; } void FITSView::loadInFrame() { // Check if the loading was OK if (fitsWatcher.result() == false) { m_LastError = imageData->getLastError(); emit failed(); return; } // Notify if there is debayer data. emit debayerToggled(imageData->hasDebayer()); if (processData()) emit loaded(); else emit failed(); } int FITSView::saveFITS(const QString &newFilename) { return imageData->saveFITS(newFilename); } bool FITSView::rescale(FITSZoom type) { switch (imageData->property("dataType").toInt()) { case TBYTE: return rescale(type); case TSHORT: return rescale(type); case TUSHORT: return rescale(type); case TLONG: return rescale(type); case TULONG: return rescale(type); case TFLOAT: return rescale(type); case TLONGLONG: return rescale(type); case TDOUBLE: return rescale(type); default: break; } return false; } FITSView::CursorMode FITSView::getCursorMode() { return cursorMode; } void FITSView::enterEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(0.2); a->setEndValue(1); a->setEasingCurve(QEasingCurve::InBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } void FITSView::leaveEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(1); a->setEndValue(0.2); a->setEasingCurve(QEasingCurve::OutBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } template bool FITSView::rescale(FITSZoom type) { if (rawImage.isNull()) return false; - - uint8_t * imageBuffer = imageData->getImageBuffer(); - uint8_t * displayBuffer = nullptr; - uint32_t size = imageData->width() * imageData->height(); - - QVector min(3), max(3); - - if (Options::autoStretch()) - { - displayBuffer = new uint8_t[size * imageData->channels() * imageData->getBytesPerPixel()]; - memcpy(displayBuffer, imageBuffer, size * imageData->channels() * imageData->getBytesPerPixel()); - imageData->applyFilter(FITS_AUTO_STRETCH, displayBuffer, &min, &max); - } - else + if (true || image_height != imageData->height() || image_width != imageData->width()) { - displayBuffer = imageBuffer; - for (int i = 0; i < 3; i++) - { - min[i] = imageData->getMin(i); - max[i] = imageData->getMax(i); - } - } - - scaledImage = QImage(); + image_width = imageData->width(); + image_height = imageData->height(); - auto * buffer = reinterpret_cast(displayBuffer); + initDisplayImage(); - if (min[0] == max[0]) - { - rawImage.fill(Qt::white); - emit newStatus(i18n("Image is saturated."), FITS_MESSAGE); + if (isVisible()) + emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); } - else - { - if (image_height != imageData->height() || image_width != imageData->width()) - { - image_width = imageData->width(); - image_height = imageData->height(); - initDisplayImage(); + image_frame->setScaledContents(true); + currentWidth = rawImage.width(); + currentHeight = rawImage.height(); - if (isVisible()) - emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); - } - - image_frame->setScaledContents(true); - currentWidth = rawImage.width(); - currentHeight = rawImage.height(); - - if (imageData->channels() == 1) - { - double range = max[0] - min[0]; - double bscale = 255. / range; - double bzero = (-min[0]) * (255. / range); - - QVector> futures; - - /* Fill in pixel values using indexed map, linear scale */ - for (uint32_t j = 0; j < image_height; j++) - { - futures.append(QtConcurrent::run([ = ]() - { - T * runningBuffer = buffer + j * image_width; - uint8_t * scanLine = rawImage.scanLine(j); - for (uint32_t i = 0; i < image_width; i++) - { - //scanLine[i] = qBound(0, static_cast(runningBuffer[i] * bscale + bzero), 255); - scanLine[i] = qBound(0.0, runningBuffer[i] * bscale + bzero, 255.0); - } - })); - } - - for(QFuture future : futures) - future.waitForFinished(); - } - else - { - QVector> futures; - double bscale[3], bzero[3]; - for (int i = 0; i < 3; i++) - { - bscale[i] = 255. / (max[i] - min[i]); - bzero[i] = (-min[i]) * (255. / (max[i] - min[i])); - } - - /* Fill in pixel values using indexed map, linear scale */ - for (uint32_t j = 0; j < image_height; j++) - { - futures.append(QtConcurrent::run([ = ]() - { - auto * scanLine = reinterpret_cast((rawImage.scanLine(j))); - T * runningBufferR = buffer + j * image_width; - T * runningBufferG = buffer + j * image_width + size; - T * runningBufferB = buffer + j * image_width + size * 2; - - for (uint32_t i = 0; i < image_width; i++) - { - scanLine[i] = qRgb(runningBufferR[i] * bscale[0] + bzero[0], - runningBufferG[i] * bscale[1] + bzero[1], - runningBufferB[i] * bscale[2] + bzero[2]); - } - })); - } - - for(QFuture future : futures) - future.waitForFinished(); - } - - } - - // Clear memory if it was allocated. - if (displayBuffer != imageBuffer) - delete [] displayBuffer; + doStretch(imageData, &rawImage, stretchImage); + + scaledImage = QImage(); switch (type) { case ZOOM_FIT_WINDOW: if ((rawImage.width() > width() || rawImage.height() > height())) { double w = baseSize().width() - BASE_OFFSET; double h = baseSize().height() - BASE_OFFSET; if (!firstLoad) { w = viewport()->rect().width() - BASE_OFFSET; h = viewport()->rect().height() - BASE_OFFSET; } // Find the zoom level which will enclose the current FITS in the current window size double zoomX = floor((w / static_cast(currentWidth)) * 100.); double zoomY = floor((h / static_cast(currentHeight)) * 100.); (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); if (currentZoom <= ZOOM_MIN) emit actionUpdated("view_zoom_out", false); } else { currentZoom = 100; currentWidth = image_width; currentHeight = image_height; } break; case ZOOM_KEEP_LEVEL: { currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); } break; default: currentZoom = 100; break; } setWidget(image_frame.get()); if (type != ZOOM_KEEP_LEVEL) emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); return true; } void FITSView::ZoomIn() { if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) { emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); return; } if (currentZoom < ZOOM_DEFAULT) currentZoom += ZOOM_LOW_INCR; else currentZoom += ZOOM_HIGH_INCR; emit actionUpdated("view_zoom_out", true); if (currentZoom >= ZOOM_MAX) { currentZoom = ZOOM_MAX; emit actionUpdated("view_zoom_in", false); } currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomOut() { if (currentZoom <= ZOOM_DEFAULT) currentZoom -= ZOOM_LOW_INCR; else currentZoom -= ZOOM_HIGH_INCR; if (currentZoom <= ZOOM_MIN) { currentZoom = ZOOM_MIN; emit actionUpdated("view_zoom_out", false); } emit actionUpdated("view_zoom_in", true); currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomToFit() { if (rawImage.isNull() == false) { rescale(ZOOM_FIT_WINDOW); updateFrame(); } } void FITSView::setStarFilterRange(float const innerRadius, float const outerRadius) { starFilter.innerRadius = innerRadius; starFilter.outerRadius = outerRadius; } int FITSView::filterStars() { return starFilter.used() ? imageData->filterStars(starFilter.innerRadius, starFilter.outerRadius) : imageData->getStarCenters().count(); } void FITSView::updateFrame() { bool ok = false; + if (toggleStretchAction) + toggleStretchAction->setChecked(stretchImage); + if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary if (scaledImage.isNull() || currentWidth != lastWidth || currentHeight != lastHeight) { scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); lastWidth = currentWidth; lastHeight = currentHeight; } ok = displayPixmap.convertFromImage(scaledImage); } else ok = displayPixmap.convertFromImage(rawImage); if (!ok) return; QPainter painter(&displayPixmap); drawOverlay(&painter); if (starFilter.used()) { double const diagonal = std::sqrt(currentWidth * currentWidth + currentHeight * currentHeight) / 2; int const innerRadius = std::lround(diagonal * starFilter.innerRadius); int const outerRadius = std::lround(diagonal * starFilter.outerRadius); QPoint const center(currentWidth / 2, currentHeight / 2); painter.save(); painter.setPen(QPen(Qt::blue, 1, Qt::DashLine)); painter.setOpacity(0.7); painter.setBrush(QBrush(Qt::transparent)); painter.drawEllipse(center, outerRadius, outerRadius); painter.setBrush(QBrush(Qt::blue, Qt::FDiagPattern)); painter.drawEllipse(center, innerRadius, innerRadius); painter.restore(); } image_frame->setPixmap(displayPixmap); image_frame->resize(currentWidth, currentHeight); } void FITSView::ZoomDefault() { if (image_frame != nullptr) { emit actionUpdated("view_zoom_out", true); emit actionUpdated("view_zoom_in", true); currentZoom = ZOOM_DEFAULT; currentWidth = image_width; currentHeight = image_height; updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter * painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) drawTrackingBox(painter); if (!markerCrosshair.isNull()) drawMarker(painter); if (showCrosshair) drawCrosshair(painter); if (showObjects) drawObjectNames(painter); if (showEQGrid) drawEQGrid(painter); if (showPixelGrid) drawPixelGrid(painter); if (markStars) drawStarCentroid(painter); } void FITSView::updateMode(FITSMode fmode) { mode = fmode; } void FITSView::drawMarker(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 2)); painter->setBrush(Qt::NoBrush); float pxperdegree = (currentZoom / ZOOM_DEFAULT) * (57.3 / 1.8); float s1 = 0.5 * pxperdegree; float s2 = pxperdegree; float s3 = 2.0 * pxperdegree; float x0 = markerCrosshair.x() * (currentZoom / ZOOM_DEFAULT); float y0 = markerCrosshair.y() * (currentZoom / ZOOM_DEFAULT); float x1 = x0 - 0.5 * s1; float y1 = y0 - 0.5 * s1; float x2 = x0 - 0.5 * s2; float y2 = y0 - 0.5 * s2; float x3 = x0 - 0.5 * s3; float y3 = y0 - 0.5 * s3; //Draw radial lines painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); //Draw circles at 0.5 & 1 degrees painter->drawEllipse(QRectF(x1, y1, s1, s1)); painter->drawEllipse(QRectF(x2, y2, s2, s2)); } void FITSView::drawStarCentroid(QPainter * painter) { float const ratio = currentZoom / ZOOM_DEFAULT; if (showStarsHFR) { QFont painterFont; // If we need to print the HFR out, give an arbitrarily sized font to the painter painterFont.setPointSizeF(painterFont.pointSizeF() * 3 * ratio); painter->setFont(painterFont); } painter->setPen(QPen(Qt::red, 2)); QFontMetrics const fontMetrics = painter->fontMetrics(); QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height()); foreach (auto const &starCenter, imageData->getStarCenters()) { int const x1 = std::round((starCenter->x - starCenter->width / 2.0f) * ratio); int const y1 = std::round((starCenter->y - starCenter->width / 2.0f) * ratio); int const w = std::round(starCenter->width * ratio); // Draw a circle around the detected star painter->drawEllipse(x1, y1, w, w); if (showStarsHFR) { // Ask the painter how large will the HFR text be QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2); QSize const hfrSize = fontMetrics.size(Qt::TextSingleLine, hfr); // Store the HFR text in a rect QPoint const hfrBottomLeft(x1+w+5, y1+w/2); QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height()); // Render the HFR text only if it can be displayed entirely if (boundingRect.contains(hfrRect)) { painter->setPen(QPen(Qt::red, 3)); painter->drawText(hfrBottomLeft, hfr); painter->setPen(QPen(Qt::red, 2)); } } } } void FITSView::drawTrackingBox(QPainter * painter) { painter->setPen(QPen(Qt::green, 2)); if (trackingBox.isNull()) return; int x1 = trackingBox.x() * (currentZoom / ZOOM_DEFAULT); int y1 = trackingBox.y() * (currentZoom / ZOOM_DEFAULT); int w = trackingBox.width() * (currentZoom / ZOOM_DEFAULT); int h = trackingBox.height() * (currentZoom / ZOOM_DEFAULT); painter->drawRect(x1, y1, w, h); } /** This Method draws a large Crosshair in the center of the image, it is like a set of axes. */ void FITSView::drawCrosshair(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); float midX = (float)image_width / 2 * scale; float midY = (float)image_height / 2 * scale; float maxX = (float)image_width * scale; float maxY = (float)image_height * scale; float r = 50 * scale; painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")))); //Horizontal Line to Circle painter->drawLine(0, midY, midX - r, midY); //Horizontal Line past Circle painter->drawLine(midX + r, midY, maxX, midY); //Vertical Line to Circle painter->drawLine(midX, 0, midX, midY - r); //Vertical Line past Circle painter->drawLine(midX, midY + r, midX, maxY); //Circles painter->drawEllipse(c, r, r); painter->drawEllipse(c, r / 2, r / 2); } /** This method is intended to draw a pixel grid onto the image. It first determines useful information from the image. Then it draws the axes on the image if the crosshairs are not displayed. Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. Note: This has to start drawing at the center not at the edges because the center axes must be in the center of the image. */ void FITSView::drawPixelGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); double width = image_width * scale; double height = image_height * scale; double cX = width / 2; double cY = height / 2; double deltaX = width / 10; double deltaY = height / 10; //draw the Axes painter->setPen(QPen(Qt::red)); painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); painter->drawText(width - 30, cY - 5, QString::number((int)((cY) / scale))); if (!showCrosshair) { painter->drawLine(cX, 0, cX, height); painter->drawLine(0, cY, width, cY); } painter->setPen(QPen(Qt::gray)); //Start one iteration past the Center and draw 4 lines on either side of 0 for (int x = deltaX; x < cX - deltaX; x += deltaX) { painter->drawText(cX + x - 30, height - 5, QString::number((int)((cX + x) / scale))); painter->drawText(cX - x - 30, height - 5, QString::number((int)((cX - x) / scale))); painter->drawLine(cX - x, 0, cX - x, height); painter->drawLine(cX + x, 0, cX + x, height); } //Start one iteration past the Center and draw 4 lines on either side of 0 for (int y = deltaY; y < cY - deltaY; y += deltaY) { painter->drawText(width - 30, cY + y - 5, QString::number((int)((cY + y) / scale))); painter->drawText(width - 30, cY - y - 5, QString::number((int)((cY - y) / scale))); painter->drawLine(0, cY + y, width, cY + y); painter->drawLine(0, cY - y, width, cY - y); } } bool FITSView::imageHasWCS() { if (imageData != nullptr) return imageData->hasWCS(); return false; } void FITSView::drawObjectNames(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); float scale = (currentZoom / ZOOM_DEFAULT); foreach (FITSSkyObject * listObject, imageData->getSkyObjects()) { painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); } } /** This method will paint EQ Gridlines in an overlay if there is WCS data present. It determines the minimum and maximum RA and DEC, then it uses that information to judge which gridLines to draw. Then it calls the drawEQGridlines methods below to draw gridlines at those specific RA and Dec values. */ void FITSView::drawEQGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); if (imageData->hasWCS()) { wcs_point * wcs_coord = imageData->getWCSCoord(); if (wcs_coord != nullptr) { int size = image_width * image_height; double maxRA = -1000; double minRA = 1000; double maxDec = -1000; double minDec = 1000; for (int i = 0; i < (size); i++) { double ra = wcs_coord[i].ra; double dec = wcs_coord[i].dec; if (ra > maxRA) maxRA = ra; if (ra < minRA) minRA = ra; if (dec > maxDec) maxDec = dec; if (dec < minDec) minDec = dec; } auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop auto maxDecMinutes = (int)(maxDec * 12); auto minRAMinutes = (int)(minRA / 15.0 * 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. if (maxDec > 50 || minDec < -50) { minRAMinutes = (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees maxRAMinutes = (int)(maxRA / 15.0 * 60.0); raConvert = 15 / 60.0; } if (maxDec > 80 || minDec < -80) { minRAMinutes = (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees maxRAMinutes = (int)(maxRA / 15.0 * 30); raConvert = 15 / 30.0; } if (maxDec > 85 || minDec < -85) { minRAMinutes = (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees maxRAMinutes = (int)(maxRA / 15.0 * 6); raConvert = 15 / 6.0; } if (maxDec >= 89.25 || minDec <= -89.25) { minRAMinutes = (int)(minRA / 15); //This will force the scale to whole hours of RA in the loop really close to the poles maxRAMinutes = (int)(maxRA / 15); raConvert = 15; } painter->setPen(QPen(Qt::yellow)); QPointF pixelPoint, imagePoint, pPoint; //This section draws the RA Gridlines for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) { painter->setPen(QPen(Qt::yellow)); double target = targetRA * raConvert; if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxDec - minDec) / 100.0); //This will determine how many points to use to create the RA Line for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) { SkyPoint pointToGet(target / 15.0, targetDec); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) { if (maxDec > 50 || maxDec < -50) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + '\''); else painter->drawText(pt.x() - 20, pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + "' " + QString::number(dms(target).second()) + "''"); } } } //This section draws the DEC Gridlines for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) { if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxRA - minRA) / 100.0); //This will determine how many points to use to create the Dec Line double target = targetDec * decConvert; for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) { SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''); } } //This Section Draws the North Celestial Pole if present SkyPoint NCP(0, 90); bool NCPtest = imageData->wcsToPixel(NCP, pPoint, imagePoint); if (NCPtest) { bool NCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (NCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("North Celestial Pole", "NCP")); } } //This Section Draws the South Celestial Pole if present SkyPoint SCP(0, -90); bool SCPtest = imageData->wcsToPixel(SCP, pPoint, imagePoint); if (SCPtest) { bool SCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (SCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("South Celestial Pole", "SCP")); } } } } } bool FITSView::pointIsInImage(QPointF pt, bool scaled) { float scale = (currentZoom / ZOOM_DEFAULT); if (scaled) return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; else return pt.x() < image_width && pt.y() < image_height && pt.x() > 0 && pt.y() > 0; } QPointF FITSView::getPointForGridLabel() { float scale = (currentZoom / ZOOM_DEFAULT); //These get the maximum X and Y points in the list that are in the image QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() > maxXPt.x() && pointIsInImage(p, true)) maxXPt = p; } QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() > maxYPt.y() && pointIsInImage(p, true)) maxYPt = p; } QPointF minXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() < minXPt.x() && pointIsInImage(p, true)) minXPt = p; } QPointF minYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() < minYPt.y() && pointIsInImage(p, true)) minYPt = p; } //This gives preference to points that are on the right hand side and bottom. //But if the line doesn't intersect the right or bottom, it then tries for the top and left. //If no points are found in the image, it returns a point off the screen //If all else fails, like in the case of a circle on the image, it returns the far right point. if (image_width * scale - maxXPt.x() < 10) { return QPointF( image_width * scale - 50, maxXPt.y() - 10); //This will draw the text on the right hand side, up and to the left of the point where the line intersects } if (image_height * scale - maxYPt.y() < 10) return QPointF( maxYPt.x() - 40, image_height * scale - 10); //This will draw the text on the bottom side, up and to the left of the point where the line intersects if (minYPt.y() * scale < 30) return QPointF( minYPt.x() + 10, 20); //This will draw the text on the top side, down and to the right of the point where the line intersects if (minXPt.x() * scale < 30) return QPointF( 10, minXPt.y() + 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) return QPointF(-100, -100); //All of the points were off the screen return QPoint(maxXPt.x() - 40, maxXPt.y() - 10); } void FITSView::setFirstLoad(bool value) { firstLoad = value; } QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) { if (trackingBox.isNull()) return trackingBoxPixmap; int x1 = (trackingBox.x() - margin) * (currentZoom / ZOOM_DEFAULT); int y1 = (trackingBox.y() - margin) * (currentZoom / ZOOM_DEFAULT); int w = (trackingBox.width() + margin * 2) * (currentZoom / ZOOM_DEFAULT); int h = (trackingBox.height() + margin * 2) * (currentZoom / ZOOM_DEFAULT); trackingBoxPixmap = image_frame->grab(QRect(x1, y1, w, h)); return trackingBoxPixmap; } void FITSView::setTrackingBox(const QRect &rect) { if (rect != trackingBox) { trackingBox = rect; updateFrame(); if(showStarProfile) viewStarProfile(); } } void FITSView::resizeTrackingBox(int newSize) { int x = trackingBox.x() + trackingBox.width() / 2; int y = trackingBox.y() + trackingBox.height() / 2; int delta = newSize / 2; setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } +bool FITSView::isImageStretched() +{ + return stretchImage; +} + bool FITSView::isCrosshairShown() { return showCrosshair; } bool FITSView::isEQGridShown() { return showEQGrid; } bool FITSView::areObjectsShown() { return showObjects; } bool FITSView::isPixelGridShown() { return showPixelGrid; } void FITSView::toggleCrosshair() { showCrosshair = !showCrosshair; updateFrame(); } void FITSView::toggleEQGrid() { showEQGrid = !showEQGrid; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleObjects() { showObjects = !showObjects; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStars() { toggleStars(!markStars); if (image_frame != nullptr) updateFrame(); } +void FITSView::toggleStretch() +{ + stretchImage = !stretchImage; + if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) + updateFrame(); +} + void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(showStarProfile) { //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views. //So for Normal and Align views, we need to set up the tracking box. if(mode == FITS_NORMAL || mode == FITS_ALIGN) { setCursorMode(selectCursor); connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); trackingBox = QRect(0, 0, 128, 128); setTrackingBoxEnabled(true); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); } else { //This shuts down the tracking box for Normal and Align Views //It doesn't touch Guide and Focus Views because they still need a tracking box if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); setTrackingBoxEnabled(false); if(starProfileWidget) disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) { disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); starProfileWidget->close(); starProfileWidget = nullptr; } emit starProfileWindowClosed(); } updateFrame(); #endif } void FITSView::move3DTrackingBox(int x, int y) { int boxSize = trackingBox.width(); QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize); setTrackingBox(starRect); } void FITSView::viewStarProfile() { #ifdef HAVE_DATAVISUALIZATION if(!trackingBoxEnabled) { setTrackingBoxEnabled(true); setTrackingBox(QRect(0, 0, 128, 128)); } if(!starProfileWidget) { starProfileWidget = new StarProfileViewer(this); //This is a band-aid to fix a QT bug with createWindowContainer //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow QWidget * superParent = this->parentWidget(); while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) superParent = superParent->parentWidget(); superParent->setCursor(Qt::ArrowCursor); //This is the end of the band-aid connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); if(mode == FITS_ALIGN || mode == FITS_NORMAL) { starProfileWidget->enableTrackingBox(true); imageData->setStarAlgorithm(ALGORITHM_CENTROID); connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } } QList starCenters = imageData->getStarCentersInSubFrame(trackingBox); if(starCenters.size() == 0) { // FIXME, the following does not work anymore. //imageData->findStars(&trackingBox, true); // FIXME replacing it with this imageData->findStars(ALGORITHM_CENTROID, trackingBox); starCenters = imageData->getStarCentersInSubFrame(trackingBox); } starProfileWidget->loadData(imageData, trackingBox, starCenters); starProfileWidget->show(); starProfileWidget->raise(); if(markStars) updateFrame(); //this is to update for the marked stars #endif } void FITSView::togglePixelGrid() { showPixelGrid = !showPixelGrid; updateFrame(); } int FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) { int count = 0; if(trackingBoxEnabled) count = imageData->findStars(algorithm, trackingBox); else count = imageData->findStars(algorithm, searchBox); return count; } void FITSView::toggleStars(bool enable) { markStars = enable; if (markStars && !imageData->areStarsSearched()) { QApplication::setOverrideCursor(Qt::WaitCursor); emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); qApp->processEvents(); int count = findStars(); if (count >= 0 && isVisible()) emit newStatus(i18np("1 star detected.", "%1 stars detected.", count), FITS_MESSAGE); QApplication::restoreOverrideCursor(); } } void FITSView::processPointSelection(int x, int y) { emit trackingStarSelected(x, y); } void FITSView::processMarkerSelection(int x, int y) { markerCrosshair.setX(x); markerCrosshair.setY(y); updateFrame(); } void FITSView::setTrackingBoxEnabled(bool enable) { if (enable != trackingBoxEnabled) { trackingBoxEnabled = enable; //updateFrame(); } } void FITSView::wheelEvent(QWheelEvent * event) { //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad //It should still do the zoom if it is a mouse wheel if (event->source() == Qt::MouseEventSynthesizedBySystem) { QScrollArea::wheelEvent(event); } else { QPoint mouseCenter = getImagePoint(event->pos()); if (event->angleDelta().y() > 0) ZoomIn(); else ZoomOut(); event->accept(); cleanUpZoom(mouseCenter); } } /** This method is intended to keep key locations in an image centered on the screen while zooming. If there is a marker or tracking box, it centers on those. If not, it uses the point called viewCenter that was passed as a parameter. */ void FITSView::cleanUpZoom(QPoint viewCenter) { int x0 = 0; int y0 = 0; double scale = (currentZoom / ZOOM_DEFAULT); if (!markerCrosshair.isNull()) { x0 = markerCrosshair.x() * scale; y0 = markerCrosshair.y() * scale; } else if (trackingBoxEnabled) { x0 = trackingBox.center().x() * scale; y0 = trackingBox.center().y() * scale; } else { x0 = viewCenter.x() * scale; y0 = viewCenter.y() * scale; } ensureVisible(x0, y0, width() / 2, height() / 2); updateMouseCursor(); } /** This method converts a point from the ViewPort Coordinate System to the Image Coordinate System. */ QPoint FITSView::getImagePoint(QPoint viewPortPoint) { QWidget * w = widget(); if (w == nullptr) return QPoint(0, 0); double scale = (currentZoom / ZOOM_DEFAULT); QPoint widgetPoint = w->mapFromParent(viewPortPoint); QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); return imagePoint; } void FITSView::initDisplayImage() { if (imageData->channels() == 1) { rawImage = QImage(image_width, image_height, QImage::Format_Indexed8); rawImage.setColorCount(256); for (int i = 0; i < 256; i++) rawImage.setColor(i, qRgb(i, i, i)); } else { rawImage = QImage(image_width, image_height, QImage::Format_RGB32); } } /** The Following two methods allow gestures to work with trackpads. Specifically, we are targeting the pinch events, so that if one is generated, Then the pinchTriggered method will be called. If the event is not a pinch gesture, then the event is passed back to the other event handlers. */ bool FITSView::event(QEvent * event) { if (event->type() == QEvent::Gesture) return gestureEvent(dynamic_cast(event)); return QScrollArea::event(event); } bool FITSView::gestureEvent(QGestureEvent * event) { if (QGesture * pinch = event->gesture(Qt::PinchGesture)) pinchTriggered(dynamic_cast(pinch)); return true; } /** This Method works with Trackpads to use the pinch gesture to scroll in and out It stores a point to keep track of the location where the gesture started so that while you are zooming, it tries to keep that initial point centered in the view. **/ void FITSView::pinchTriggered(QPinchGesture * gesture) { if (!zooming) { zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); zooming = true; } if (gesture->state() == Qt::GestureFinished) { zooming = false; } zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. if (zoomTime > 10000) //This ensures zoomtime never gets too big. zoomTime = 0; if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. { if (gesture->totalScaleFactor() > 1) ZoomIn(); else ZoomOut(); } cleanUpZoom(zoomLocation); } /*void FITSView::handleWCSCompletion() { //bool hasWCS = wcsWatcher.result(); if(imageData->hasWCS()) this->updateFrame(); emit wcsToggled(imageData->hasWCS()); }*/ void FITSView::syncWCSState() { bool hasWCS = imageData->hasWCS(); bool wcsLoaded = imageData->isWCSLoaded(); if (hasWCS && wcsLoaded) this->updateFrame(); emit wcsToggled(hasWCS); if (toggleEQGridAction != nullptr) toggleEQGridAction->setEnabled(hasWCS); if (toggleObjectsAction != nullptr) toggleObjectsAction->setEnabled(hasWCS); if (centerTelescopeAction != nullptr) centerTelescopeAction->setEnabled(hasWCS); } void FITSView::createFloatingToolBar() { if (floatingToolBar != nullptr) return; floatingToolBar = new QToolBar(this); auto * eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); eff->setOpacity(0.2); floatingToolBar->setVisible(false); floatingToolBar->setStyleSheet( "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" "QToolButton{background: transparent; border:none; color: yellow}" "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); floatingToolBar->setFloatable(true); floatingToolBar->setIconSize(QSize(25, 25)); //floatingToolBar->setMovable(true); QAction * action = nullptr; floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this, SLOT(ZoomIn())); floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this, SLOT(ZoomOut())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), i18n("Default Zoom"), this, SLOT(ZoomDefault())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); + toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"), + i18n("Toggle Stretch"), + this, SLOT(toggleStretch())); + toggleStretchAction->setCheckable(true); + + floatingToolBar->addSeparator(); action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); action->setCheckable(true); action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); action->setCheckable(true); toggleStarsAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), i18n("Detect Stars in Image"), this, SLOT(toggleStars())); toggleStarsAction->setCheckable(true); #ifdef HAVE_DATAVISUALIZATION toggleProfileAction = floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), i18n("View Star Profile"), this, SLOT(toggleStarProfile())); toggleProfileAction->setCheckable(true); #endif if (mode == FITS_NORMAL || mode == FITS_ALIGN) { floatingToolBar->addSeparator(); toggleEQGridAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid())); toggleEQGridAction->setCheckable(true); toggleEQGridAction->setEnabled(false); toggleObjectsAction = floatingToolBar->addAction(QIcon::fromTheme("help-hint"), i18n("Show Objects in Image"), this, SLOT(toggleObjects())); toggleObjectsAction->setCheckable(true); toggleEQGridAction->setEnabled(false); centerTelescopeAction = floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), i18n("Center Telescope"), this, SLOT(centerTelescope())); centerTelescopeAction->setCheckable(true); centerTelescopeAction->setEnabled(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSView::centerTelescope() { if (imageHasWCS()) { if (getCursorMode() == FITSView::scopeCursor) { setCursorMode(lastMouseMode); } else { lastMouseMode = getCursorMode(); setCursorMode(FITSView::scopeCursor); } updateFrame(); } updateScopeButton(); } void FITSView::updateScopeButton() { if (centerTelescopeAction != nullptr) { if (getCursorMode() == FITSView::scopeCursor) { centerTelescopeAction->setChecked(true); } else { centerTelescopeAction->setChecked(false); } } } /** This method just verifies if INDI is online, a telescope present, and is connected */ bool FITSView::isTelescopeActive() { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { return false; } foreach (ISD::GDInterface * gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice * bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; return bd->isConnected(); } return false; #else return false; #endif } void FITSView::setStarsEnabled(bool enable) { markStars = enable; if (floatingToolBar != nullptr) { foreach (QAction * action, floatingToolBar->actions()) { if (action->text() == i18n("Detect Stars in Image")) { action->setChecked(markStars); break; } } } } void FITSView::setStarsHFREnabled(bool enable) { showStarsHFR = enable; } diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index 7287562e6..53fdd14b6 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,359 +1,367 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "fitscommon.h" #include #ifdef HAVE_DATAVISUALIZATION #include "starprofileviewer.h" #endif #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include #include #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QAction; class QEvent; class QGestureEvent; class QImage; class QLabel; class QPinchGesture; class QResizeEvent; class QToolBar; class FITSData; class FITSLabel; class FITSView : public QScrollArea { Q_OBJECT public: explicit FITSView(QWidget *parent = nullptr, FITSMode fitsMode = FITS_NORMAL, FITSScale filterType = FITS_NONE); virtual ~FITSView() override; typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and displays it in a FITSView frame * @param inFilename FITS File name * @param silent if set, error popups are suppressed. * @note If image is successfully, loaded() signal is emitted, otherwise failed() signal is emitted. * Obtain error by calling lastError() */ void loadFITS(const QString &inFilename, bool silent = true); /** * @brief loadFITSFromData Takes ownership of the FITSData instance passed in and displays it in a FITSView frame * @param inFilename FITS File name to use */ bool loadFITSFromData(FITSData *data, const QString &inFilename); // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired bool rescale(FITSZoom type); // Access functions FITSData *getImageData() const { return imageData; } double getCurrentZoom() const { return currentZoom; } QImage getDisplayImage() const { return rawImage; } const QPixmap &getDisplayPixmap() const { return displayPixmap; } // Tracking square void setTrackingBoxEnabled(bool enable); bool isTrackingBoxEnabled() const { return trackingBoxEnabled; } QPixmap &getTrackingBoxPixmap(uint8_t margin = 0); void setTrackingBox(const QRect &rect); const QRect &getTrackingBox() const { return trackingBox; } // last error const QString &lastError() const { return m_LastError; } // Overlay virtual void drawOverlay(QPainter *); // Overlay objects void drawStarCentroid(QPainter *); void drawTrackingBox(QPainter *); void drawMarker(QPainter *); void drawCrosshair(QPainter *); void drawEQGrid(QPainter *); void drawObjectNames(QPainter *painter); void drawPixelGrid(QPainter *painter); + bool isImageStretched(); bool isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); + // Setup the graphics. void updateFrame(); bool isTelescopeActive(); void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; CursorMode getCursorMode(); void setCursorMode(CursorMode mode); void updateMouseCursor(); void updateScopeButton(); void setScopeButton(QAction *action) { centerTelescopeAction = action; } // Zoom related void cleanUpZoom(QPoint viewCenter); QPoint getImagePoint(QPoint viewPortPoint); uint16_t zoomedWidth() { return currentWidth; } uint16_t zoomedHeight() { return currentHeight; } // Star Detection int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &searchBox = QRect()); void toggleStars(bool enable); void setStarsEnabled(bool enable); void setStarsHFREnabled(bool enable); void setStarFilterRange(float const innerRadius, float const outerRadius); int filterStars(); // FITS Mode void updateMode(FITSMode fmode); FITSMode getMode() { return mode; } void setFilter(FITSScale newFilter) { filter = newFilter; } void setFirstLoad(bool value); void pushFilter(FITSScale value) { filterStack.push(value); } FITSScale popFilter() { return filterStack.pop(); } CursorMode lastMouseMode { selectCursor }; bool isStarProfileShown() { return showStarProfile; } // Floating toolbar void createFloatingToolBar(); //void setLoadWCSEnabled(bool value); public slots: void wheelEvent(QWheelEvent *event) override; void resizeEvent(QResizeEvent *event) override; void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); // Grids void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggleCrosshair(); // Stars void toggleStars(); void toggleStarProfile(); void viewStarProfile(); void centerTelescope(); + void toggleStretch(); + virtual void processPointSelection(int x, int y); virtual void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); void resizeTrackingBox(int newSize); protected slots: /** * @brief syncWCSState Update toolbar and actions depending on whether WCS is available or not */ void syncWCSState(); bool event(QEvent *event) override; bool gestureEvent(QGestureEvent *event); void pinchTriggered(QPinchGesture *gesture); protected: template bool rescale(FITSZoom type); double average(); double stddev(); void calculateMaxPixel(double min, double max); void initDisplayImage(); QPointF getPointForGridLabel(); bool pointIsInImage(QPointF pt, bool scaled); void loadInFrame(); /// WCS Future Watcher QFutureWatcher wcsWatcher; /// FITS Future Watcher QFutureWatcher fitsWatcher; /// Cross hair QPointF markerCrosshair; /// Pointer to FITSData object FITSData *imageData { nullptr }; /// Current zoom level double currentZoom { 0 }; private: bool processData(); QLabel *noImageLabel { nullptr }; QPixmap noImage; QVector eqGridPoints; std::unique_ptr image_frame; uint32_t image_width { 0 }; uint32_t image_height { 0 }; /// Current width due to zoom uint16_t currentWidth { 0 }; uint16_t lastWidth { 0 }; /// Current height due to zoom uint16_t currentHeight { 0 }; uint16_t lastHeight { 0 }; /// Image zoom factor const double zoomFactor; // Original full-size image QImage rawImage; // Scaled images QImage scaledImage; // Actual pixmap after all the overlays QPixmap displayPixmap; bool firstLoad { true }; bool markStars { false }; bool showStarProfile { false }; bool showCrosshair { false }; bool showObjects { false }; bool showEQGrid { false }; bool showPixelGrid { false }; bool showStarsHFR { false }; + bool stretchImage { false }; + struct { bool used() const { return innerRadius != 0.0f || outerRadius != 1.0f; } float innerRadius { 0.0f }; float outerRadius { 1.0f }; } starFilter; CursorMode cursorMode { selectCursor }; bool zooming { false }; int zoomTime { 0 }; QPoint zoomLocation; QString filename; FITSMode mode; FITSScale filter; QString m_LastError; QStack filterStack; // Tracking box bool trackingBoxEnabled { false }; QRect trackingBox; QPixmap trackingBoxPixmap; // Scope pixmap QPixmap redScopePixmap; // Magenta Scope Pixmap QPixmap magentaScopePixmap; // Floating toolbar QToolBar *floatingToolBar { nullptr }; QAction *centerTelescopeAction { nullptr }; QAction *toggleEQGridAction { nullptr }; QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; + QAction *toggleStretchAction { nullptr }; + //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION QPointer starProfileWidget; #endif signals: void newStatus(const QString &msg, FITSBar id); void debayerToggled(bool); void wcsToggled(bool); void actionUpdated(const QString &name, bool enable); void trackingStarSelected(int x, int y); void loaded(); void failed(); void starProfileWindowClosed(); friend class FITSLabel; }; diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp index 8f490cc46..ba11c933d 100644 --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -1,1015 +1,1033 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com 2006-03-03 Using CFITSIO, Porting to Qt4 ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "fitsviewer.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsdebayer.h" #include "fitstab.h" #include "fitsview.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #ifdef HAVE_INDI #include "indi/indilistener.h" #endif #include #include #include #include #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #define INITIAL_W 785 #define INITIAL_H 640 QStringList FITSViewer::filterTypes = QStringList() << I18N_NOOP("Auto Stretch") << I18N_NOOP("High Contrast") << I18N_NOOP("Equalize") << I18N_NOOP("High Pass") << I18N_NOOP("Median") << I18N_NOOP("Rotate Right") << I18N_NOOP("Rotate Left") << I18N_NOOP("Flip Horizontal") << I18N_NOOP("Flip Vertical"); FITSViewer::FITSViewer(QWidget *parent) : KXmlGuiWindow(parent) { #ifdef Q_OS_OSX if (Options::independentWindowFITS()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #endif fitsTabWidget = new QTabWidget(this); undoGroup = new QUndoGroup(this); lastURL = QUrl(QDir::homePath()); fitsTabWidget->setTabsClosable(true); setWindowIcon(QIcon::fromTheme("kstars_fitsviewer")); setCentralWidget(fitsTabWidget); connect(fitsTabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabFocusUpdated(int))); connect(fitsTabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int))); //These two connections will enable or disable the scope button if a scope is available or not. //Of course this is also dependent on the presence of WCS data in the image. #ifdef HAVE_INDI connect(INDIListener::Instance(), SIGNAL(newTelescope(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); connect(INDIListener::Instance(), SIGNAL(deviceRemoved(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); #endif led.setColor(Qt::green); fitsPosition.setAlignment(Qt::AlignCenter); fitsValue.setAlignment(Qt::AlignCenter); //fitsPosition.setFixedWidth(100); //fitsValue.setFixedWidth(100); fitsWCS.setVisible(false); statusBar()->insertPermanentWidget(FITS_WCS, &fitsWCS); statusBar()->insertPermanentWidget(FITS_VALUE, &fitsValue); statusBar()->insertPermanentWidget(FITS_POSITION, &fitsPosition); statusBar()->insertPermanentWidget(FITS_ZOOM, &fitsZoom); statusBar()->insertPermanentWidget(FITS_RESOLUTION, &fitsResolution); statusBar()->insertPermanentWidget(FITS_LED, &led); QAction *action = actionCollection()->addAction("rotate_right", this, SLOT(rotateCW())); action->setText(i18n("Rotate Right")); action->setIcon(QIcon::fromTheme("object-rotate-right")); action = actionCollection()->addAction("rotate_left", this, SLOT(rotateCCW())); action->setText(i18n("Rotate Left")); action->setIcon(QIcon::fromTheme("object-rotate-left")); action = actionCollection()->addAction("flip_horizontal", this, SLOT(flipHorizontal())); action->setText(i18n("Flip Horizontal")); action->setIcon( QIcon::fromTheme("object-flip-horizontal")); action = actionCollection()->addAction("flip_vertical", this, SLOT(flipVertical())); action->setText(i18n("Flip Vertical")); action->setIcon(QIcon::fromTheme("object-flip-vertical")); action = actionCollection()->addAction("image_histogram"); action->setText(i18n("Histogram")); connect(action, SIGNAL(triggered(bool)), SLOT(histoFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); action->setIcon(QIcon(":/icons/histogram.png")); action = KStandardAction::open(this, SLOT(openFile()), actionCollection()); action->setIcon(QIcon::fromTheme("document-open")); saveFileAction = KStandardAction::save(this, SLOT(saveFile()), actionCollection()); saveFileAction->setIcon(QIcon::fromTheme("document-save")); saveFileAsAction = KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection()); saveFileAsAction->setIcon( QIcon::fromTheme("document-save_as")); action = actionCollection()->addAction("fits_header"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_H)); action->setIcon(QIcon::fromTheme("document-properties")); action->setText(i18n("FITS Header")); connect(action, SIGNAL(triggered(bool)), SLOT(headerFITS())); action = actionCollection()->addAction("fits_debayer"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D)); action->setIcon(QIcon::fromTheme("view-preview")); action->setText(i18n("Debayer...")); connect(action, SIGNAL(triggered(bool)), SLOT(debayerFITS())); action = actionCollection()->addAction("image_stretch"); - action->setText(i18n("Auto stretch")); - connect(action, SIGNAL(triggered(bool)), SLOT(stretchFITS())); + action->setText(i18n("Toggle Auto stretch")); + action->setCheckable(true); + connect(action, SIGNAL(triggered(bool)), SLOT(toggleStretch())); actionCollection()->setDefaultShortcut(action, QKeySequence::SelectAll); - action->setIcon(QIcon::fromTheme("transform-move")); + action->setIcon(QIcon::fromTheme("transform-move")); action = KStandardAction::close(this, SLOT(close()), actionCollection()); action->setIcon(QIcon::fromTheme("window-close")); action = KStandardAction::copy(this, SLOT(copyFITS()), actionCollection()); action->setIcon(QIcon::fromTheme("edit-copy")); action = KStandardAction::zoomIn(this, SLOT(ZoomIn()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-in")); action = KStandardAction::zoomOut(this, SLOT(ZoomOut()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-out")); action = KStandardAction::actualSize(this, SLOT(ZoomDefault()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-fit-best")); QAction *kundo = KStandardAction::undo(undoGroup, SLOT(undo()), actionCollection()); kundo->setIcon(QIcon::fromTheme("edit-undo")); QAction *kredo = KStandardAction::redo(undoGroup, SLOT(redo()), actionCollection()); kredo->setIcon(QIcon::fromTheme("edit-redo")); connect(undoGroup, SIGNAL(canUndoChanged(bool)), kundo, SLOT(setEnabled(bool))); connect(undoGroup, SIGNAL(canRedoChanged(bool)), kredo, SLOT(setEnabled(bool))); action = actionCollection()->addAction("image_stats"); action->setIcon(QIcon::fromTheme("view-statistics")); action->setText(i18n("Statistics")); connect(action, SIGNAL(triggered(bool)), SLOT(statFITS())); action = actionCollection()->addAction("view_crosshair"); action->setIcon(QIcon::fromTheme("crosshairs")); action->setText(i18n("Show Cross Hairs")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleCrossHair())); action = actionCollection()->addAction("view_pixel_grid"); action->setIcon(QIcon::fromTheme("map-flat")); action->setText(i18n("Show Pixel Gridlines")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(togglePixelGrid())); action = actionCollection()->addAction("view_eq_grid"); action->setIcon(QIcon::fromTheme("kstars_grid")); action->setText(i18n("Show Equatorial Gridlines")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleEQGrid())); action = actionCollection()->addAction("view_objects"); action->setIcon(QIcon::fromTheme("help-hint")); action->setText(i18n("Show Objects in Image")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleObjects())); action = actionCollection()->addAction("center_telescope"); action->setIcon(QIcon(":/icons/center_telescope.svg")); action->setText(i18n("Center Telescope\n*No Telescopes Detected*")); action->setDisabled(true); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(centerTelescope())); action = actionCollection()->addAction("view_zoom_fit"); action->setIcon(QIcon::fromTheme("zoom-fit-width")); action->setText(i18n("Zoom To Fit")); connect(action, SIGNAL(triggered(bool)), SLOT(ZoomToFit())); #ifdef HAVE_DATAVISUALIZATION action = actionCollection()->addAction("toggle_3D_graph"); action->setIcon(QIcon::fromTheme("star_profile", QIcon(":/icons/star_profile.svg"))); action->setText(i18n("View 3D Graph")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggle3DGraph())); #endif action = actionCollection()->addAction("mark_stars"); action->setText(i18n("Mark Stars")); connect(action, SIGNAL(triggered(bool)), SLOT(toggleStars())); int filterCounter = 1; for (auto& filter : FITSViewer::filterTypes) { action = actionCollection()->addAction(QString("filter%1").arg(filterCounter)); action->setText(i18n(filter.toUtf8().constData())); connect(action, &QAction::triggered, this, [this, filterCounter] { applyFilter(filterCounter);}); filterCounter++; } /* Create GUI */ createGUI("fitsviewerui.rc"); setWindowTitle(i18n("KStars FITS Viewer")); /* initially resize in accord with KDE rules */ show(); resize(INITIAL_W, INITIAL_H); } void FITSViewer::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } FITSViewer::~FITSViewer() { // if (KStars::Instance()) // { // for (QPointer fv : KStars::Instance()->getFITSViewersList()) // { // if (fv.data() == this) // { // KStars::Instance()->getFITSViewersList().removeOne(this); // break; // } // } // } fitsTabWidget->disconnect(); qDeleteAll(fitsTabs); fitsTabs.clear(); } void FITSViewer::closeEvent(QCloseEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); QList viewers = KStars::Instance()->findChildren(); if (a && viewers.count() == 1) { a->setEnabled(false); a->setChecked(false); } } } void FITSViewer::hideEvent(QHideEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { QList viewers = KStars::Instance()->findChildren(); if (viewers.count() <= 1) a->setChecked(false); } } } void FITSViewer::showEvent(QShowEvent * /*event*/) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { a->setEnabled(true); a->setChecked(true); } } bool FITSViewer::addFITSCommon(FITSTab *tab, const QUrl &imageName, FITSMode mode, const QString &previewText) { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex != -1) return false; lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); QApplication::restoreOverrideCursor(); tab->setPreviewText(previewText); // Connect tab signals connect(tab, &FITSTab::newStatus, this, &FITSViewer::updateStatusBar); connect(tab, &FITSTab::changeStatus, this, &FITSViewer::updateTabStatus); connect(tab, &FITSTab::debayerToggled, this, &FITSViewer::setDebayerAction); // Connect tab view signals connect(tab->getView(), &FITSView::actionUpdated, this, &FITSViewer::updateAction); connect(tab->getView(), &FITSView::wcsToggled, this, &FITSViewer::updateWCSFunctions); connect(tab->getView(),&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); switch (mode) { case FITS_NORMAL: fitsTabWidget->addTab(tab, previewText.isEmpty() ? imageName.fileName() : previewText); break; case FITS_CALIBRATE: fitsTabWidget->addTab(tab, i18n("Calibrate")); break; case FITS_FOCUS: fitsTabWidget->addTab(tab, i18n("Focus")); break; case FITS_GUIDE: fitsTabWidget->addTab(tab, i18n("Guide")); break; case FITS_ALIGN: fitsTabWidget->addTab(tab, i18n("Align")); break; } saveFileAction->setEnabled(true); saveFileAsAction->setEnabled(true); undoGroup->addStack(tab->getUndoStack()); fitsTabs.push_back(tab); fitsMap[fitsID] = tab; fitsTabWidget->setCurrentWidget(tab); actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); tab->tabPositionUpdated(); tab->setUID(fitsID); led.setColor(Qt::green); updateStatusBar(i18n("Ready."), FITS_MESSAGE); tab->getView()->setCursorMode(FITSView::dragCursor); + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), tab->getView()->isImageStretched()); + updateWCSFunctions(); return true; } void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); connect(tab, &FITSTab::failed, [&]() { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); }); connect(tab, &FITSTab::loaded, [=]() { if (addFITSCommon(tab, imageName, mode, previewText)) emit loaded(fitsID++); }); tab->loadFITS(imageName, mode, filter, silent); } bool FITSViewer::addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, FITSMode mode, FITSScale filter, const QString &previewText) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); if (!tab->loadFITSFromData(data, imageName, mode, filter)) { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); return false; } if (!addFITSCommon(tab, imageName, mode, previewText)) return false; *tab_uid = fitsID++; return true; } bool FITSViewer::removeFITS(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; return false; } int index = fitsTabs.indexOf(tab); if (index >= 0) { closeTab(index); return true; } return false; } void FITSViewer::updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter, bool silent) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; emit failed(); return; } if (tab->isVisible()) led.setColor(Qt::yellow); // On tab load success auto conn = std::make_shared(); *conn = connect(tab, &FITSTab::loaded, this, [=]() { if (updateFITSCommon(tab, imageName)) { QObject::disconnect(*conn); emit loaded(tab->getUID()); } }); tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); } bool FITSViewer::updateFITSCommon(FITSTab *tab, const QUrl &imageName) { // On tab load success int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex == -1) return false; if (tab->getView()->getMode() == FITS_NORMAL) { if ((imageName.path().startsWith(QLatin1String("/tmp")) || imageName.path().contains("/Temp")) && Options::singlePreviewFITS()) fitsTabWidget->setTabText(tabIndex, tab->getPreviewText().isEmpty() ? i18n("Preview") : tab->getPreviewText()); else fitsTabWidget->setTabText(tabIndex, imageName.fileName()); } tab->getUndoStack()->clear(); if (tab->isVisible()) led.setColor(Qt::green); return true; } bool FITSViewer::updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, int *tab_uid, FITSScale filter) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) return false; if (tab->isVisible()) led.setColor(Qt::yellow); if (!tab->loadFITSFromData(data, imageName, tab->getView()->getMode(), filter)) return false; if (!updateFITSCommon(tab, imageName)) return false; *tab_uid = tab->getUID(); return true; } void FITSViewer::tabFocusUpdated(int currentIndex) { if (currentIndex < 0 || fitsTabs.empty()) return; fitsTabs[currentIndex]->tabPositionUpdated(); FITSView *view = fitsTabs[currentIndex]->getView(); view->toggleStars(markStars); if (isVisible()) view->updateFrame(); if (markStars) updateStatusBar(i18np("%1 star detected.", "%1 stars detected.", view->getImageData()->getDetectedStars()), FITS_MESSAGE); else updateStatusBar("", FITS_MESSAGE); if (view->getImageData()->hasDebayer()) { actionCollection()->action("fits_debayer")->setEnabled(true); if (debayerDialog) { BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); } } else actionCollection()->action("fits_debayer")->setEnabled(false); updateStatusBar("", FITS_WCS); connect(view,&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), getCurrentView()->isStarProfileShown()); updateButtonStatus("view_crosshair", i18n("Cross Hairs"), getCurrentView()->isCrosshairShown()); updateButtonStatus("view_eq_grid", i18n("Equatorial Gridines"), getCurrentView()->isEQGridShown()); updateButtonStatus("view_objects", i18n("Objects in Image"), getCurrentView()->areObjectsShown()); updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), getCurrentView()->isPixelGridShown()); + + fprintf(stderr, "Updating button status to %s\n", getCurrentView()->isImageStretched() ? "true" : "false"); + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), getCurrentView()->isImageStretched()); updateScopeButton(); updateWCSFunctions(); } void FITSViewer::starProfileButtonOff() { updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), false); } void FITSViewer::openFile() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open FITS Image"), lastURL, "FITS (*.fits *.fits.fz *.fit *.fts)"); if (fileURL.isEmpty()) return; lastURL = QUrl(fileURL.url(QUrl::RemoveFilename)); QString fpath = fileURL.toLocalFile(); QString cpath; // Make sure we don't have it open already, if yes, switch to it foreach (FITSTab *tab, fitsTabs) { cpath = tab->getCurrentURL()->path(); if (fpath == cpath) { fitsTabWidget->setCurrentWidget(tab); return; } } addFITS(fileURL, FITS_NORMAL, FITS_NONE, QString(), false); } void FITSViewer::saveFile() { fitsTabs[fitsTabWidget->currentIndex()]->saveFile(); } void FITSViewer::saveFileAs() { if (fitsTabs.empty()) return; if (fitsTabs[fitsTabWidget->currentIndex()]->saveFileAs() && fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() == FITS_NORMAL) fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), fitsTabs[fitsTabWidget->currentIndex()]->getCurrentURL()->fileName()); } void FITSViewer::copyFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->copyFITS(); } void FITSViewer::histoFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->histoFITS(); } void FITSViewer::statFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->statFITS(); } -void FITSViewer::stretchFITS() -{ - applyFilter(FITS_AUTO_STRETCH); -} - void FITSViewer::rotateCW() { applyFilter(FITS_ROTATE_CW); } void FITSViewer::rotateCCW() { applyFilter(FITS_ROTATE_CCW); } void FITSViewer::flipHorizontal() { applyFilter(FITS_FLIP_H); } void FITSViewer::flipVertical() { applyFilter(FITS_FLIP_V); } void FITSViewer::headerFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->headerFITS(); } void FITSViewer::debayerFITS() { if (debayerDialog == nullptr) { debayerDialog = new FITSDebayer(this); } FITSView *view = getCurrentView(); if (view == nullptr) return; BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); debayerDialog->show(); } void FITSViewer::updateStatusBar(const QString &msg, FITSBar id) { switch (id) { case FITS_POSITION: fitsPosition.setText(msg); break; case FITS_RESOLUTION: fitsResolution.setText(msg); break; case FITS_ZOOM: fitsZoom.setText(msg); break; case FITS_WCS: fitsWCS.setVisible(true); fitsWCS.setText(msg); break; case FITS_VALUE: fitsValue.setText(msg); break; case FITS_MESSAGE: statusBar()->showMessage(msg); break; default: break; } } void FITSViewer::ZoomIn() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomIn(); } void FITSViewer::ZoomOut() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomOut(); } void FITSViewer::ZoomDefault() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomDefault(); } void FITSViewer::ZoomToFit() { if (fitsTabs.empty()) return; getCurrentView()->ZoomToFit(); } void FITSViewer::updateAction(const QString &name, bool enable) { QAction *toolAction = actionCollection()->action(name); if (toolAction != nullptr) toolAction->setEnabled(enable); } void FITSViewer::updateTabStatus(bool clean) { if (fitsTabs.empty() || (fitsTabWidget->currentIndex() >= fitsTabs.size())) return; if (fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() != FITS_NORMAL) return; //QString tabText = fitsImages[fitsTab->currentIndex()]->getCurrentURL()->fileName(); QString tabText = fitsTabWidget->tabText(fitsTabWidget->currentIndex()); fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), clean ? tabText.remove('*') : tabText + '*'); } void FITSViewer::closeTab(int index) { if (fitsTabs.empty()) return; FITSTab *tab = fitsTabs[index]; int UID = tab->getUID(); fitsMap.remove(UID); fitsTabs.removeOne(tab); delete tab; if (fitsTabs.empty()) { saveFileAction->setEnabled(false); saveFileAsAction->setEnabled(false); } emit closed(UID); } /** This is helper function to make it really easy to make the update the state of toggle buttons that either show or hide information in the Current view. This method would get called both when one of them gets pushed and also when tabs are switched. */ void FITSViewer::updateButtonStatus(const QString& action, const QString& item, bool showing) { QAction *a = actionCollection()->action(action); if (a == nullptr) return; if (showing) { a->setText(i18n("Hide %1", item)); a->setChecked(true); } else { a->setText(i18n("Show %1", item)); a->setChecked(false); } } /** This is a method that either enables or disables the WCS based features in the Current View. */ void FITSViewer::updateWCSFunctions() { if (getCurrentView() == nullptr) return; if (getCurrentView()->imageHasWCS()) { actionCollection()->action("view_eq_grid")->setDisabled(false); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines")); actionCollection()->action("view_objects")->setDisabled(false); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image")); if (getCurrentView()->isTelescopeActive()) { actionCollection()->action("center_telescope")->setDisabled(false); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*Ready*")); } else { actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No Telescopes Detected*")); } } else { actionCollection()->action("view_eq_grid")->setDisabled(true); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines\n*No WCS Info*")); actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No WCS Info*")); actionCollection()->action("view_objects")->setDisabled(true); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image\n*No WCS Info*")); } } void FITSViewer::updateScopeButton() { if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { actionCollection()->action("center_telescope")->setChecked(true); } else { actionCollection()->action("center_telescope")->setChecked(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSViewer::centerTelescope() { getCurrentView()->setScopeButton(actionCollection()->action("center_telescope")); if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { getCurrentView()->setCursorMode(getCurrentView()->lastMouseMode); } else { getCurrentView()->lastMouseMode = getCurrentView()->getCursorMode(); getCurrentView()->setCursorMode(FITSView::scopeCursor); } updateScopeButton(); } void FITSViewer::toggleCrossHair() { if (fitsTabs.empty()) return; getCurrentView()->toggleCrosshair(); updateButtonStatus("view_crosshair", i18n("Cross Hairs"), getCurrentView()->isCrosshairShown()); } void FITSViewer::toggleEQGrid() { if (fitsTabs.empty()) return; getCurrentView()->toggleEQGrid(); updateButtonStatus("view_eq_grid", i18n("Equatorial Gridlines"), getCurrentView()->isEQGridShown()); } void FITSViewer::toggleObjects() { if (fitsTabs.empty()) return; getCurrentView()->toggleObjects(); updateButtonStatus("view_objects", i18n("Objects in Image"), getCurrentView()->areObjectsShown()); } void FITSViewer::togglePixelGrid() { if (fitsTabs.empty()) return; getCurrentView()->togglePixelGrid(); updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), getCurrentView()->isPixelGridShown()); } void FITSViewer::toggle3DGraph() { if (fitsTabs.empty()) return; getCurrentView()->toggleStarProfile(); updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), getCurrentView()->isStarProfileShown()); } void FITSViewer::toggleStars() { if (markStars) { markStars = false; actionCollection()->action("mark_stars")->setText(i18n("Mark Stars")); } else { markStars = true; actionCollection()->action("mark_stars")->setText(i18n("Unmark Stars")); } foreach (FITSTab *tab, fitsTabs) { tab->getView()->toggleStars(markStars); tab->getView()->updateFrame(); } } void FITSViewer::applyFilter(int ftype) { if (fitsTabs.empty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); updateStatusBar(i18n("Processing %1...", filterTypes[ftype - 1]), FITS_MESSAGE); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getHistogram()->applyFilter(static_cast(ftype)); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getView()->updateFrame(); QApplication::restoreOverrideCursor(); updateStatusBar(i18n("Ready."), FITS_MESSAGE); } +void FITSViewer::toggleStretch() +{ + if (fitsTabs.empty()) + return; + + QApplication::setOverrideCursor(Qt::WaitCursor); + updateStatusBar(i18n("Processing toggle stretch"), FITS_MESSAGE); + qApp->processEvents(); + fitsTabs[fitsTabWidget->currentIndex()]->getView()->toggleStretch(); + + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), + getCurrentView()->isImageStretched()); + + QApplication::restoreOverrideCursor(); + updateStatusBar(i18n("Ready."), FITS_MESSAGE); +} + FITSView *FITSViewer::getView(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab) return tab->getView(); return nullptr; } FITSView *FITSViewer::getCurrentView() { if (fitsTabs.empty() || fitsTabWidget->currentIndex() >= fitsTabs.count()) return nullptr; return fitsTabs[fitsTabWidget->currentIndex()]->getView(); } void FITSViewer::setDebayerAction(bool enable) { actionCollection()->addAction("fits_debayer")->setEnabled(enable); } diff --git a/kstars/fitsviewer/fitsviewer.h b/kstars/fitsviewer/fitsviewer.h index e1b0bf07d..5374b00c3 100644 --- a/kstars/fitsviewer/fitsviewer.h +++ b/kstars/fitsviewer/fitsviewer.h @@ -1,163 +1,163 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "fitscommon.h" #include #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include class QCloseEvent; class QUndoGroup; class QTabWidget; class FITSDebayer; class FITSTab; class FITSView; class FITSData; /** * @class FITSViewer * @short Primary window to view monochrome and color FITS images. * The FITSviewer can open multiple images each in a separate. It supports simple filters, histogram transforms, flip and rotation operations, and star detection. * * @author Jasem Mutlaq * @version 1.0 */ class FITSViewer : public KXmlGuiWindow { Q_OBJECT public: /** Constructor. */ explicit FITSViewer(QWidget *parent); ~FITSViewer(); void addFITS(const QUrl &imageName, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString(), bool silent = true); bool addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString()); void updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter = FITS_NONE, bool silent = true); bool updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, int *tab_uid, FITSScale filter = FITS_NONE); bool removeFITS(int fitsUID); bool isStarsMarked() { return markStars; } bool empty() const { return fitsTabs.empty(); } QList getTabs() { return fitsTabs; } FITSView *getView(int fitsUID); FITSView *getCurrentView(); static QStringList filterTypes; protected: void closeEvent(QCloseEvent *) override; void hideEvent(QHideEvent *) override; void showEvent(QShowEvent *) override; public slots: void changeAlwaysOnTop(Qt::ApplicationState state); void openFile(); void saveFile(); void saveFileAs(); void copyFITS(); void statFITS(); void headerFITS(); void debayerFITS(); void histoFITS(); - void stretchFITS(); void tabFocusUpdated(int currentIndex); void updateStatusBar(const QString &msg, FITSBar id); void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); void updateAction(const QString &name, bool enable); void updateTabStatus(bool clean); void closeTab(int index); void toggleStars(); void toggleCrossHair(); void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggle3DGraph(); void starProfileButtonOff(); void centerTelescope(); void updateWCSFunctions(); void applyFilter(int ftype); + void toggleStretch(); void rotateCW(); void rotateCCW(); void flipHorizontal(); void flipVertical(); void setDebayerAction(bool); void updateScopeButton(); private: void updateButtonStatus(const QString &action, const QString &item, bool showing); // Shared utilites between the standard and "FromData" addFITS and updateFITS. bool addFITSCommon(FITSTab *tab, const QUrl &imageName, FITSMode mode, const QString &previewText); bool updateFITSCommon(FITSTab *tab, const QUrl &imageName); QTabWidget *fitsTabWidget { nullptr }; QUndoGroup *undoGroup { nullptr }; FITSDebayer *debayerDialog { nullptr }; KLed led; QLabel fitsPosition, fitsValue, fitsResolution, fitsZoom, fitsWCS; QAction *saveFileAction { nullptr }; QAction *saveFileAsAction { nullptr }; QList fitsTabs; int fitsID { 0 }; bool markStars { false }; QMap fitsMap; QUrl lastURL; signals: void trackingStarSelected(int x, int y); void loaded(int tabUID); void closed(int tabUID); void failed(); }; diff --git a/kstars/fitsviewer/stretch.cpp b/kstars/fitsviewer/stretch.cpp new file mode 100644 index 000000000..418d20980 --- /dev/null +++ b/kstars/fitsviewer/stretch.cpp @@ -0,0 +1,415 @@ +#include "stretch.h" + +/* Stretch + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. +*/ + +#include "stretch.h" + +#include +#include +#include + +namespace { + +// Returns the median v of the vector. +// The vector is modified in an undefined way. +template +T median(std::vector& values) +{ + const int middle = values.size() / 2; + std::nth_element(values.begin(), values.begin() + middle, values.end()); + return values[middle]; +} + +// Returns the median of the sample values. +// The values are not modified. +template +T median(T *values, int size, int sampleBy) +{ + const int downsampled_size = size / sampleBy; + std::vector samples(downsampled_size); + for (int index = 0, i = 0; index < size; ++i, index += sampleBy) + samples[i] = values[index]; + return median(samples); +} + +// This stretches one channel given the input parameters. +// Based on the spec in section 8.5.6 +// http://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html +// Uses multiple threads, blocks until done. +// The extension parameters are not used. +template +void stretchOneChannel(T *input_buffer, QImage *output_image, + const StretchParams& stretch_params, + int input_range, int image_height, int image_width) +{ + QVector> futures; + + // We're outputting uint8, so the max output is 255. + constexpr int maxOutput = 255; + + // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). + const float maxInput = input_range > 1 ? input_range - 1 : input_range; + + const float midtones = stretch_params.grey_red.midtones; + const float highlights = stretch_params.grey_red.highlights; + const float shadows = stretch_params.grey_red.shadows; + + // Precomputed expressions moved out of the loop. + // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. + const float hsRangeFactor = highlights == shadows ? 1.0 : 1.0 / (highlights - shadows); + // Shadow and highlight values translated to the ADU scale. + const T nativeShadows = shadows * maxInput; + const T nativeHighlights = highlights * maxInput; + // Constants based on above needed for the stretch calculations. + const float k1 = (midtones - 1) * hsRangeFactor * maxOutput / maxInput; + const float k2 = ((2 * midtones) - 1) * hsRangeFactor / maxInput; + + for (int j = 0; j < image_height; j++) + { + futures.append(QtConcurrent::run([ = ]() + { + T * inputLine = input_buffer + j * image_width; + auto * scanLine = output_image->scanLine(j); + + for (int i = 0; i < image_width; i++) + { + const T input = inputLine[i]; + if (input < nativeShadows) scanLine[i] = 0; + else if (input >= nativeHighlights) scanLine[i] = maxOutput; + else + { + const T inputFloored = (input - nativeShadows); + scanLine[i] = (inputFloored * k1) / (inputFloored * k2 - midtones); + } + } + })); + } + for(QFuture future : futures) + future.waitForFinished(); +} + +// This is like the above 1-channel stretch, but extended for 3 channels. +// This could have been more modular, but the three channels are combined +// into a single qRgb value at the end, so it seems the simplest thing is to +// replicate the code. It is assume the colors are not interleaved--the red image +// is stored fully, then the green, then the blue. +template +void stretchThreeChannels(T *inputBuffer, QImage *outputImage, + const StretchParams& stretchParams, + int inputRange, int imageHeight, int imageWidth) +{ + QVector> futures; + + // We're outputting uint8, so the max output is 255. + constexpr int maxOutput = 255; + + // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). + const float maxInput = inputRange > 1 ? inputRange - 1 : inputRange; + + const float midtonesR = stretchParams.grey_red.midtones; + const float highlightsR = stretchParams.grey_red.highlights; + const float shadowsR = stretchParams.grey_red.shadows; + const float midtonesG = stretchParams.green.midtones; + const float highlightsG = stretchParams.green.highlights; + const float shadowsG = stretchParams.green.shadows; + const float midtonesB = stretchParams.blue.midtones; + const float highlightsB = stretchParams.blue.highlights; + const float shadowsB = stretchParams.blue.shadows; + + // Precomputed expressions moved out of the loop. + // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. + const float hsRangeFactorR = highlightsR == shadowsR ? 1.0 : 1.0 / (highlightsR - shadowsR); + const float hsRangeFactorG = highlightsG == shadowsG ? 1.0 : 1.0 / (highlightsG - shadowsG); + const float hsRangeFactorB = highlightsB == shadowsB ? 1.0 : 1.0 / (highlightsB - shadowsB); + // Shadow and highlight values translated to the ADU scale. + const T nativeShadowsR = shadowsR * maxInput; + const T nativeShadowsG = shadowsG * maxInput; + const T nativeShadowsB = shadowsB * maxInput; + const T nativeHighlightsR = highlightsR * maxInput; + const T nativeHighlightsG = highlightsG * maxInput; + const T nativeHighlightsB = highlightsB * maxInput; + // Constants based on above needed for the stretch calculations. + const float k1R = (midtonesR - 1) * hsRangeFactorR * maxOutput / maxInput; + const float k1G = (midtonesG - 1) * hsRangeFactorG * maxOutput / maxInput; + const float k1B = (midtonesB - 1) * hsRangeFactorB * maxOutput / maxInput; + const float k2R = ((2 * midtonesR) - 1) * hsRangeFactorR / maxInput; + const float k2G = ((2 * midtonesG) - 1) * hsRangeFactorG / maxInput; + const float k2B = ((2 * midtonesB) - 1) * hsRangeFactorB / maxInput; + + const int size = imageWidth * imageHeight; + + for (int j = 0; j < imageHeight; j++) + { + futures.append(QtConcurrent::run([ = ]() + { + // R, G, B input images are stored one after another. + T * inputLineR = inputBuffer + j * imageWidth; + T * inputLineG = inputLineR + size; + T * inputLineB = inputLineG + size; + + auto * scanLine = reinterpret_cast(outputImage->scanLine(j)); + + for (int i = 0; i < imageWidth; i++) + { + const T inputR = inputLineR[i]; + const T inputG = inputLineG[i]; + const T inputB = inputLineB[i]; + + uint8_t red, green, blue; + + if (inputR < nativeShadowsR) red = 0; + else if (inputR >= nativeHighlightsR) red = maxOutput; + else + { + const T inputFloored = (inputR - nativeShadowsR); + red = (inputFloored * k1R) / (inputFloored * k2R - midtonesR); + } + + if (inputG < nativeShadowsG) green = 0; + else if (inputG >= nativeHighlightsG) green = maxOutput; + else + { + const T inputFloored = (inputG - nativeShadowsG); + green = (inputFloored * k1G) / (inputFloored * k2G - midtonesG); + } + + if (inputB < nativeShadowsB) blue = 0; + else if (inputB >= nativeHighlightsB) blue = maxOutput; + else + { + const T inputFloored = (inputB - nativeShadowsB); + blue = (inputFloored * k1B) / (inputFloored * k2B - midtonesB); + } + scanLine[i] = qRgb(red, green, blue); + } + })); + } + for(QFuture future : futures) + future.waitForFinished(); +} + +template +void stretchChannels(T *input_buffer, QImage *output_image, + const StretchParams& stretch_params, + int input_range, int image_height, int image_width, int num_channels) +{ + if (num_channels == 1) + stretchOneChannel(input_buffer, output_image, stretch_params, input_range, + image_height, image_width); + else if (num_channels == 3) + stretchThreeChannels(input_buffer, output_image, stretch_params, input_range, + image_height, image_width); +} + +// See section 8.5.7 in above link http://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html +template +void computeParamsOneChannel(T *buffer, StretchParams1Channel *params, + int inputRange, int height, int width) +{ + // Find the median sample. + constexpr int maxSamples = 500000; + const int sampleBy = width * height < maxSamples ? 1 : width * height / maxSamples; + const int size = width * height; + T medianSample = median(buffer, width * height, sampleBy); + + // Find the Median deviation: 1.4826 * median of abs(sample[i] - median). + const int numSamples = width * height / sampleBy; + std::vector deviations(numSamples); + for (int index = 0, i = 0; index < size; ++i, index += sampleBy) + { + if (medianSample > buffer[index]) + deviations[i] = medianSample - buffer[index]; + else + deviations[i] = buffer[index] - medianSample; + } + + // Shift everything to 0 -> 1.0. + const float medDev = median(deviations); + const float normalizedMedian = medianSample / static_cast(inputRange); + const float MADN = 1.4826 * medDev / static_cast(inputRange); + + const bool upperHalf = normalizedMedian > 0.5; + + const float shadows = (upperHalf || MADN == 0) ? 0.0 : + fmin(1.0, fmax(0.0, (normalizedMedian + -2.8 * MADN))); + + const float highlights = (!upperHalf || MADN == 0) ? 1.0 : + fmin(1.0, fmax(0.0, (normalizedMedian - -2.8 * MADN))); + + float X, M; + constexpr float B = 0.25; + if (!upperHalf) { + X = normalizedMedian - shadows; + M = B; + } else { + X = B; + M = highlights - normalizedMedian; + } + float midtones; + if (X == 0) midtones = 0; + else if (X == M) midtones = 0.5; + else if (X == 1) midtones = 1.0; + else midtones = ((M - 1) * X) / ((2 * M - 1) * X - M); + + // Store the params. + params->shadows = shadows; + params->highlights = highlights; + params->midtones = midtones; + params->shadows_expansion = 0.0; + params->highlights_expansion = 1.0; +} + +// Need to know the possible range of input values. +// Using the type of the sample and guessing. +// Perhaps we should examine the contents for the file +// (e.g. look at maximum value and extrapolate from that). +int getRange(int data_type) +{ + switch (data_type) + { + case TBYTE: + return 256; + break; + case TSHORT: + return 64*1024; + break; + case TUSHORT: + return 64*1024; + break; + case TLONG: + return 64*1024; + break; + case TFLOAT: + return 64*1024; + break; + case TLONGLONG: + return 64*1024; + break; + case TDOUBLE: + return 64*1024; + break; + default: + return 64*1024; + break; + } +} + +} // namespace + +Stretch::Stretch(int width, int height, int channels, int data_type) +{ + image_width = width; + image_height = height; + image_channels = channels; + dataType = data_type; + input_range = getRange(dataType); +} + +void Stretch::run(uint8_t *input, QImage *outputImage) +{ + switch (dataType) + { + case TBYTE: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TSHORT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TUSHORT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TLONG: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TFLOAT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TLONGLONG: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TDOUBLE: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + default: + break; + } +} + +StretchParams Stretch::computeParams(uint8_t *input) +{ + StretchParams result; + for (int channel = 0; channel < image_channels; ++channel) + { + int offset = channel * image_width * image_height; + StretchParams1Channel *params = channel == 0 ? &result.grey_red : + (channel == 1 ? &result.green : &result.blue); + switch (dataType) + { + case TBYTE: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TSHORT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TUSHORT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TLONG: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TFLOAT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TLONGLONG: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TDOUBLE: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + default: + break; + } + } + return result; +} diff --git a/kstars/fitsviewer/stretch.h b/kstars/fitsviewer/stretch.h new file mode 100644 index 000000000..3a2ca32c0 --- /dev/null +++ b/kstars/fitsviewer/stretch.h @@ -0,0 +1,90 @@ +/* Stretch + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. +*/ + +#pragma once + +#include +#include + +struct StretchParams1Channel +{ + // Stretch algorithm parameters + float shadows;; + float highlights; + float midtones; + // The extension parameters are not yet used. + float shadows_expansion; + float highlights_expansion; + + // The default parameters result in no stretch at all. + StretchParams1Channel() + { + shadows = 0.0; + highlights = 1.0; + midtones = 0.5; + shadows_expansion = 0.0; + highlights_expansion = 1.0; + } +}; + +struct StretchParams +{ + StretchParams1Channel grey_red, green, blue; +}; + +class Stretch +{ + public: + /** + * @brief Stretch Constructor for Stretch class + * @param image_buffer pointer to the image memory + * @param width the image width + * @param height the image height + * @param channels should be 1 or 3 + * @note The image should either be 1-channel or 3-channel + * The image buffer is not copied, so it should not be deleted while the object is in use + */ + explicit Stretch(int width, int height, int channels, int data_type); + ~Stretch() {} + + /** + * @brief setParams Sets the stretch parameters. + * @param param The desired parameter values. + * @note This set method used for both 1-channel and 3-channel images. + * In 1-channel images, the _g and _b parameters are ignored. + * The parameter scale is 0-1 for all data types. + */ + void setParams(StretchParams input_params) { params = input_params; } + + /** + * @brief getParams Returns the stretch parameters (computed by computeParameters()). + */ + StretchParams getParams() { return params; } + + /** + * @brief computeParams Automatically generates and sets stretch parameters from the image. + */ + StretchParams computeParams(uint8_t *input); + + /** + * @brief run run the stretch algorithm according to the params given + * placing the output in output_image. + */ + void run(uint8_t *input, QImage *output_image); + + private: + // Inputs. + int image_width; + int image_height; + int image_channels; + int input_range; + int dataType; + + // Parameters. + StretchParams params; +};