From 7dbecfeda967e93d09c8c32f83d8c83469596b51 Mon Sep 17 00:00:00 2001 From: roby Date: Thu, 13 Aug 2026 15:54:20 -0600 Subject: [PATCH] Firefly-2058: Add update to become FITS aware - Fits file extraction - Fits readout - Fits cube integration - Wcs lock will support syncing cube planes --- .../ipac/firefly/data/ServerParams.java | 1 + .../caltech/ipac/firefly/server/SrvParam.java | 10 + .../firefly/server/rpc/VisServerCommands.java | 23 +- .../server/servlets/AnyFileDownload.java | 2 +- .../server/servlets/AnyFileUpload.java | 2 +- .../firefly/server/servlets/HiPSRetrieve.java | 252 +++++++++++++----- .../firefly/server/util/LockingRetrieve.java | 22 +- .../server/visualize/FluxValueUtil.java | 100 +++++++ .../server/visualize/ImagePlotBuilder.java | 25 +- .../server/visualize/VisJsonSerializer.java | 1 + .../server/visualize/VisServerOps.java | 31 +-- .../server/visualize/WebPlotFactory.java | 13 +- .../server/visualize/hips/HiPSListUtil.java | 65 ++++- .../visualize/hips/IrsaHiPSListSource.java | 45 +--- .../visualize/hips/LsstHiPSListSource.java | 60 +---- .../imageretrieve/URIFileRetriever.java | 59 +++- .../edu/caltech/ipac/util/FitsHDUUtil.java | 8 +- .../edu/caltech/ipac/util/FormatUtil.java | 6 + .../ipac/util/download/RetrieveUtil.java | 6 +- .../ipac/util/download/UriRefParams.java | 9 +- .../ipac/visualize/plot/ImagePlot.java | 27 +- .../ipac/visualize/plot/PixelValue.java | 8 +- .../visualize/plot/plotdata/FitsExtract.java | 2 +- .../visualize/plot/plotdata/FitsRead.java | 2 +- .../visualize/plot/plotdata/FitsReadUtil.java | 43 ++- src/firefly/js/core/BootstrapRegistry.js | 3 +- src/firefly/js/data/ServerParams.js | 1 + .../js/drawingLayers/ExtractHiPSTileTool.js | 203 ++++++++++++++ .../js/drawingLayers/ExtractPointsTool.js | 26 +- .../js/drawingLayers/PointSelection.js | 3 +- src/firefly/js/rpc/PlotServicesJson.js | 50 ++-- src/firefly/js/ui/HiPSImageSelect.jsx | 2 +- src/firefly/js/ui/tap/DataServicesOptions.js | 1 - src/firefly/js/visualize/ChangePrime.js | 22 +- src/firefly/js/visualize/HiPSUtil.js | 119 ++++++++- src/firefly/js/visualize/ImagePlotCntlr.js | 7 +- src/firefly/js/visualize/MenuItemKeys.js | 1 + src/firefly/js/visualize/MouseReadoutCntlr.js | 1 + src/firefly/js/visualize/PlotAttribute.js | 5 + src/firefly/js/visualize/PlotViewUtil.js | 57 ++-- src/firefly/js/visualize/RangeValues.js | 5 + src/firefly/js/visualize/VisMouseSync.js | 1 + src/firefly/js/visualize/WebPlot.js | 19 +- src/firefly/js/visualize/WebPlotRequest.js | 3 +- .../js/visualize/reducer/HandlePlotChange.js | 4 +- .../visualize/reducer/HandlePlotCreation.js | 10 +- .../js/visualize/saga/MouseReadoutWatch.js | 70 +++-- src/firefly/js/visualize/task/PlotHipsTask.js | 19 +- src/firefly/js/visualize/task/WcsMatchTask.js | 34 ++- src/firefly/js/visualize/ui/Buttons.jsx | 3 + src/firefly/js/visualize/ui/ColorDialog.jsx | 2 +- .../js/visualize/ui/ExtractionWatchers.js | 8 +- .../js/visualize/ui/FitsHeaderView.jsx | 4 +- .../visualize/ui/MouseReadoutBottomLine.jsx | 13 +- .../js/visualize/ui/MouseReadoutUIUtil.js | 3 +- .../js/visualize/ui/VisCtxToolbarView.jsx | 45 ++-- .../js/visualize/ui/VisMiniToolbar.jsx | 51 ++-- .../ui/extraction/ExtractionDialog.jsx | 83 ++---- .../ui/extraction/ExtractionTable.jsx | 8 +- .../ui/extraction/ExtractionUIUtil.js | 74 +++++ .../extraction/HiPSTileExtractionDialog.jsx | 145 ++++++++++ 61 files changed, 1416 insertions(+), 511 deletions(-) create mode 100644 src/firefly/java/edu/caltech/ipac/firefly/server/visualize/FluxValueUtil.java create mode 100644 src/firefly/js/drawingLayers/ExtractHiPSTileTool.js create mode 100644 src/firefly/js/visualize/ui/extraction/ExtractionUIUtil.js create mode 100644 src/firefly/js/visualize/ui/extraction/HiPSTileExtractionDialog.jsx diff --git a/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java b/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java index f8bc1df324..63d5d68e68 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java @@ -99,6 +99,7 @@ public class ServerParams { public static final String LSST = "lsst"; public static final String ALL = "all"; public static final String CDS = "cds"; + public static final String IS_HIPS_TILE = "isHipsTile"; public static final String HIPS_SOURCES = "hipsSources"; public static final String HIPS_LIST_SOURCE= "hipsListSource"; public static final String HIPS_LIST_SOURCE_NAME= "hipsListSourceName"; diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/SrvParam.java b/src/firefly/java/edu/caltech/ipac/firefly/server/SrvParam.java index a5d5d899c7..6aedf8d162 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/SrvParam.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/SrvParam.java @@ -286,6 +286,16 @@ public ImagePt getRequiredImagePt(String key) { } } + public WorldPt getRequiredWorldPt(String key) { + String v= getRequired(key); + try { + return WorldPt.parse(v); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "parameter could not be parsed as WorldPt: parameter: "+ key + ", value: "+v, e); + } + } + public ImagePt[] getRequiredImagePtAry(String key) { String v= getRequired(key); try { diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/rpc/VisServerCommands.java b/src/firefly/java/edu/caltech/ipac/firefly/server/rpc/VisServerCommands.java index bf2234825d..39b858cf33 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/rpc/VisServerCommands.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/rpc/VisServerCommands.java @@ -4,13 +4,13 @@ package edu.caltech.ipac.firefly.server.rpc; import edu.caltech.ipac.firefly.core.Util; -import static edu.caltech.ipac.util.FormatUtil.Format.*; import edu.caltech.ipac.firefly.data.ServerParams; import edu.caltech.ipac.firefly.server.ServCommand; import edu.caltech.ipac.firefly.server.ServerCommandAccess; import edu.caltech.ipac.firefly.server.SrvParam; import edu.caltech.ipac.firefly.server.util.Logger; import edu.caltech.ipac.firefly.server.visualize.DirectStretchUtils.CompressType; +import edu.caltech.ipac.firefly.server.visualize.FluxValueUtil; import edu.caltech.ipac.firefly.server.visualize.VisJsonSerializer; import edu.caltech.ipac.firefly.server.visualize.VisServerOps; import edu.caltech.ipac.firefly.server.visualize.imagesources.ImageMasterData; @@ -19,7 +19,6 @@ import edu.caltech.ipac.firefly.visualize.WebPlotRequest; import edu.caltech.ipac.firefly.visualize.WebPlotResult; import edu.caltech.ipac.visualize.plot.ImagePt; -import edu.caltech.ipac.visualize.plot.PixelValue; import edu.caltech.ipac.visualize.plot.plotdata.FitsExtract; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; @@ -35,6 +34,9 @@ import java.util.List; import java.util.Map; +import static edu.caltech.ipac.util.FormatUtil.Format.JSON; +import static edu.caltech.ipac.util.FormatUtil.Format.OCTET_STREAM; + /** * @author Trey Roby * Date: 2/8/12 @@ -44,9 +46,20 @@ public class VisServerCommands { public static class FileFluxCmdJson extends ServCommand { public String doCommand(SrvParam sp) throws IllegalArgumentException { - PlotState[] stateAry= sp.getStateAry(); - List res= VisServerOps.getFlux(stateAry,sp.getRequiredImagePt("pt")); - return VisJsonSerializer.createPixelResultJson(res,stateAry[0].getBands(),stateAry.length); + var isHips= sp.getOptionalBoolean(ServerParams.IS_HIPS_TILE, false); + if (isHips) { + var res= FluxValueUtil.getFluxHiPS( + sp.getRequired(ServerParams.URL), + sp.getRequiredImagePt("pt"), + sp.getRequiredWorldPt("wpt"), + sp.getRequiredInt(ServerParams.PLANE)); + return VisJsonSerializer.createPixelResultJson(res,new Band[] {Band.NO_BAND},1); + } + else { + var stateAry= sp.getStateAry(); + var res= FluxValueUtil.getFlux(sp.getStateAry(),sp.getRequiredImagePt("pt")); + return VisJsonSerializer.createPixelResultJson(res,stateAry[0].getBands(),stateAry.length); + } } } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileDownload.java b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileDownload.java index f35d5f9669..1c0e7e9946 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileDownload.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileDownload.java @@ -127,7 +127,7 @@ private void handleHiPSRequest(HttpServletRequest req, HttpServletResponse res, SrvParam sp= new SrvParam(req.getParameterMap()); String hips= sp.getRequired(HIPS_PARAM); boolean alwaysUseCached= sp.getOptionalBoolean(ALWAYS_USE_CACHED,false); - FileInfo fi= HiPSRetrieve.retrieveHiPSData(hips, null, alwaysUseCached); + FileInfo fi= HiPSRetrieve.retrieveHiPSData(hips, alwaysUseCached); if (fi.getFile()==null) { if (fi.getResponseCode()==204) { diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileUpload.java b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileUpload.java index 14ea897324..4047810e4a 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileUpload.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/AnyFileUpload.java @@ -246,7 +246,7 @@ private static Result retrieveFile(SrvParam sp, FileItemInput uploadedItem) thro } else if (fromUrl != null) { // from a URL... get it String fname; if (hipsCache) { - statusFileInfo= HiPSRetrieve.retrieveHiPSData(fromUrl,null,false); + statusFileInfo= HiPSRetrieve.retrieveHiPSData(fromUrl,false); fname= (statusFileInfo.getFile()!=null) ? statusFileInfo.getFile().getName() : null; } else { diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/HiPSRetrieve.java b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/HiPSRetrieve.java index 9b8f34fe83..aef6498eca 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/HiPSRetrieve.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/servlets/HiPSRetrieve.java @@ -8,25 +8,39 @@ import edu.caltech.ipac.firefly.data.FileInfo; import edu.caltech.ipac.firefly.server.ServerContext; +import edu.caltech.ipac.firefly.server.util.LockingRetrieve; +import edu.caltech.ipac.util.FormatUtil; +import edu.caltech.ipac.util.FormatUtil.Format; +import edu.caltech.ipac.util.download.DownloadListener; import edu.caltech.ipac.util.download.FailedRequestException; import edu.caltech.ipac.util.download.URLDownload; +import edu.caltech.ipac.util.download.UriRefParams; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static edu.caltech.ipac.util.FileUtil.K; import static edu.caltech.ipac.util.FileUtil.isDirectoryEmpty; +import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; +import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; /** * @author Trey Roby @@ -34,96 +48,204 @@ public class HiPSRetrieve { private static final List extList= Arrays.asList("jpg", "jpeg", "png", "webp"); + private static final List formatList= Arrays.asList(Format.JPEG,Format.PNG,Format.WEBP); private static final long minFileLengthOnError = 2*K; + private static final long minFitsFileLengthOnError = 5*K; + private static final long minSizeCacheFile = K; - public static FileInfo retrieveHiPSData(String urlStr, String pathExt, boolean alwaysUseCached) { - try { - URL url= new URL(urlStr); + private static final Map deadFitsUrl= Collections.synchronizedMap(new HashMap<>()); + private static final int MAX_DEAD_URL_SIZE= 10000; + private static final ExecutorService executor = Executors.newFixedThreadPool(10); - String fPath = pathExt == null ? url.getPath() : (url.getPath() + "/" + pathExt); - File dir= new File(ServerContext.getHiPSDir(),new File(url.getHost() + fPath).getParent()); - if (!dir.exists()) dir.mkdirs(); + public static FileInfo retrieveHiPSData(String urlStr, boolean alwaysUseCached) { + return retrieveHiPSData(urlStr,null, alwaysUseCached); - File targetFile= new File(dir, new File((pathExt == null ? url.getFile() : pathExt)).getName()); + } - if (targetFile.canRead() && targetFile.isDirectory()) { - if (isDirectoryEmpty(targetFile)) { - targetFile.delete(); - } - else { - return new FileInfo(HttpURLConnection.HTTP_FORBIDDEN, "this hips request conflicts with the HiPS protocol, attempt replace a directory with a file"); - } - } + public static FileInfo retrieveHiPSData(String urlStr, String fileName, boolean alwaysUseCached) { + URL url= URLDownload.makeURL(urlStr); + if (url==null) return new FileInfo(404, "Invalid URL: " + urlStr); + return retrieveHiPSData(url, fileName, alwaysUseCached, null); + } - boolean fileExistLocal= targetFile.canRead() && targetFile.length()>400; + public static FileInfo retrieveHiPSData(UriRefParams params) { + URL url= params.getUriRef().getURL(); + var dl= LockingRetrieve.makeDownloadProgressOrFind(params); + return retrieveHiPSData(url, null, !params.getCheckForNewer(), dl); + } + /** + * + * @param url the url + * @param fileName add a filename to the cacheName, if null then compute a name + s @param alwaysUseCached if true and the file is in the cache then don't make a If-Modified-Since call, just return + * @param dl the download listener + * @return the retrieved (or cached) file + */ + public static FileInfo retrieveHiPSData(URL url, String fileName, boolean alwaysUseCached, DownloadListener dl) { + if (url==null) return new FileInfo(404, "Invalid URL"); + + File targetFile = getHipsCacheFile(url, fileName); + var locationPrepared= prepareTargetLocation(targetFile); + + if (!locationPrepared) { + return new FileInfo(HttpURLConnection.HTTP_FORBIDDEN, + "this hips request conflicts with the HiPS protocol, cannot cache hips file correctly"); + } + boolean fileExistLocal= isHiPSFileCached(url.toString(),fileName); - FileInfo preFetchFileInfo= new FileInfo(targetFile); - if (alwaysUseCached && fileExistLocal) return preFetchFileInfo; + if (alwaysUseCached && fileExistLocal) return new FileInfo(targetFile); - // if we already have a version of the file set the download modified only option. Also set a very time timeout, - // so that if the server is down we don't wait long. - URLDownload.Options options= fileExistLocal ? URLDownload.Options.modifiedAndTimeoutOp(true,4) : URLDownload.Options.def(); - int rCode; - File retFile; - FileInfo fetchedFileInfo; - try { - fetchedFileInfo= URLDownload.getDataToFile(url,targetFile,null, null, options); - rCode= fetchedFileInfo.getResponseCode(); - retFile= fetchedFileInfo.getFile(); - } - catch (FailedRequestException e) { - if (fileExistLocal && targetFile.length() > minFileLengthOnError) return preFetchFileInfo; // if the file existed and has content, return it - else return new FileInfo(e.getResponseCode()); - } + // if we already have a version of the file set the download modified only option. Also set a very small timeout, + // so that if the server is down we don't wait long. + try { + URLDownload.Options options= fileExistLocal ? URLDownload.Options.modifiedAndTimeoutOp(true,3) : URLDownload.Options.def(); + options.setDl(dl); + FileInfo fetchedFileInfo= lockUrlDownload(url,targetFile,options); + var rCode= fetchedFileInfo.getResponseCode(); + return switch (rCode) { + case HTTP_OK, HTTP_NOT_MODIFIED -> fetchedFileInfo; + case HTTP_NOT_FOUND -> cleanupNoFound(targetFile,url); + case HTTP_CLIENT_TIMEOUT, 429, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, // transient failures, use cache if valid + HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT -> + isValidCached(targetFile) ? new FileInfo(targetFile) : cleanupBadRequest(targetFile,rCode); + default -> cleanupBadRequest(targetFile,rCode); + }; + } + catch (FailedRequestException e) { + if (isValidCached(targetFile)) return new FileInfo(targetFile); // if the file existed and has content, return it + else return new FileInfo(e.getResponseCode()); + } - switch (rCode) { - case HTTP_OK -> { - if (isValid(retFile)) return fetchedFileInfo; - if (retFile!=null) retFile.delete(); - return new FileInfo(rCode); - } - case HTTP_NOT_MODIFIED -> { - return fetchedFileInfo; - } - default -> { - if (fileExistLocal && targetFile.length() > minFileLengthOnError) return preFetchFileInfo; // if the file existed and has content, return it - if (retFile != null) retFile.delete(); - if (rCode == 404 && imageRequest(retFile)) return new FileInfo(204); - else return new FileInfo(rCode); - } + } + + private static FileInfo cleanupBadRequest(File f, int rCode) { + if (f != null) f.delete(); + return new FileInfo(rCode); + } + + private static FileInfo cleanupNoFound(File f, URL url) { + if (f != null) f.delete(); + if (url.getPath().toLowerCase().endsWith(".fits")) { + deadFitsUrl.put(url.toString(), System.currentTimeMillis() + 10 * 60 * 1000); + } + return imageRequest(f) ? new FileInfo(HTTP_NO_CONTENT) : new FileInfo(HTTP_NOT_FOUND); // return 204 because: the request was valid, but there is no image content to return + } + + private static boolean prepareTargetLocation(File targetFile) { + File dir= targetFile.getParentFile(); + if (!dir.exists() && !dir.mkdirs()) { + return false; + } + + if (!targetFile.exists()) return true; + + if (targetFile.isDirectory()) { + if (isDirectoryEmpty(targetFile)) { + if (!targetFile.delete()) return false; + } + else { + return false; } - } catch (MalformedURLException e) { - return new FileInfo(null, null, 404, e.toString()); } + if (targetFile.length() URLDownload.getDataToFile(url,targetFile,null, null, options)); + } + + public static void retrieveHiPSTileInBackground(String urlStr) { + if (isRetrieving(urlStr)) return; + executor.execute(() -> retrieveHiPSData(urlStr,false)); + } + + public static boolean isRetrieving(String urlStr) { + return LockingRetrieve.isActiveRequest(urlStr); + } + + public static File getHipsCacheFile(URL url, String fileName) { + if (url==null) return null; + String fPath = fileName == null ? url.getPath() : (url.getPath() + "/" + fileName); + File dir= new File(ServerContext.getHiPSDir(),new File(url.getHost() + fPath).getParent()); + return new File(dir, new File((fileName == null ? url.getFile() : fileName)).getName()); + } + + public static boolean isHiPSFileCached(String urlStr) { + return isHiPSFileCached(urlStr,null); + } + + public static boolean isHiPSFileCached(String urlStr, String fileName) { + if (isRetrieving(urlStr)) return false; + URL url= URLDownload.makeURL(urlStr); + if (url==null) return false; + File f= getHipsCacheFile(url,fileName); + if (f==null) return false; + return f.canRead() && f.length()>minSizeCacheFile; } private static boolean imageRequest(File f) { if (f==null) return false; String lowerF= f.getAbsolutePath().toLowerCase(); - return extList.stream().anyMatch(lowerF::contains); + return extList.stream().anyMatch(ext -> lowerF.endsWith("." + ext)); } - private static boolean isValid(File f) { - if (f==null) return false; + /** + * If we are getting errors then do some basic validation on the cached file + * @param cachedFile the cache file on disk + * @return true if valid + */ + private static boolean isValidCached(File cachedFile) { + if (cachedFile==null) return false; + if (!cachedFile.canRead()) return false; + if (cachedFile.length()= minFitsFileLengthOnError; } - } catch (IOException e) { return false; } - return true; } + public static boolean isDeadFitsUrl(String urlStr) { + var expireTime= deadFitsUrl.get(urlStr); + if (expireTime == null) return false; + if (deadFitsUrl.size() > MAX_DEAD_URL_SIZE) cleanDeadFitsUrlCache(); + if (System.currentTimeMillis() > expireTime) { + deadFitsUrl.remove(urlStr); + return false; + } + return true; + } + private synchronized static void cleanDeadFitsUrlCache() { + if (deadFitsUrl.size() <= MAX_DEAD_URL_SIZE) return; + var copyMap= new HashMap<>(deadFitsUrl); + var cleanedUpMap= new HashMap(); + var cTime= System.currentTimeMillis(); + deadFitsUrl.clear(); + copyMap.forEach((key, value) -> { + if (cTime RetrieveUtil.downloadCaching(params, makeDownloadProgressOrFind(params))); } -//====================================================================== -//----------------------- Private Methods ------------------------------ -//====================================================================== - - private static FileInfo lockingRetrieve(BaseNetParams params, Callable getter) throws FailedRequestException { + public static FileInfo lockingRetrieve(BaseNetParams params, Callable getter) throws FailedRequestException { try { Object lockKey= activeRequest.computeIfAbsent(params, k -> new Object()); synchronized (lockKey) { @@ -91,7 +87,14 @@ private static FileInfo lockingRetrieve(BaseNetParams params, Callable } } - private static DownloadProgress makeDownloadProgressOrFind(UriRefParams params) { // todo: generalize beyond just plotId + public static boolean isActiveRequest(BaseNetParams params) { + return activeRequest.containsKey(params); + } + + public static boolean isActiveRequest(String urlStr) { return isActiveRequest(new UriRefParams(urlStr)); + } + + public static DownloadProgress makeDownloadProgressOrFind(UriRefParams params) { // todo: generalize beyond just plotId if (params==null || params.getStatusKey()== null) return null; DownloadProgress dl; @@ -106,6 +109,9 @@ private static DownloadProgress makeDownloadProgressOrFind(UriRefParams params) return dl; } +//====================================================================== +//----------------------- Private Methods ------------------------------ +//====================================================================== private static FileInfo retrieveImageService(ImageServiceParams params, ServiceCaller svcCaller) throws IOException, FailedRequestException { FileInfo fileInfo= FileCacheHelper.getFileInfo(params); @@ -116,7 +122,7 @@ private static FileInfo retrieveImageService(ImageServiceParams params, ServiceC return fileInfo; } - private static class DownloadProgress implements DownloadListener { + public static class DownloadProgress implements DownloadListener { private final String key; private final List plotIdList= new ArrayList<>(); diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/FluxValueUtil.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/FluxValueUtil.java new file mode 100644 index 0000000000..34b11cb315 --- /dev/null +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/FluxValueUtil.java @@ -0,0 +1,100 @@ +package edu.caltech.ipac.firefly.server.visualize; + +import edu.caltech.ipac.firefly.server.ServerContext; +import edu.caltech.ipac.firefly.server.servlets.HiPSRetrieve; +import edu.caltech.ipac.firefly.visualize.Band; +import edu.caltech.ipac.firefly.visualize.BandState; +import edu.caltech.ipac.firefly.visualize.PlotState; +import edu.caltech.ipac.util.download.FailedRequestException; +import edu.caltech.ipac.util.download.URLDownload; +import edu.caltech.ipac.visualize.plot.ImagePlot; +import edu.caltech.ipac.visualize.plot.ImagePt; +import edu.caltech.ipac.visualize.plot.PixelValue; +import edu.caltech.ipac.visualize.plot.ProjectionException; +import edu.caltech.ipac.visualize.plot.WorldPt; +import edu.caltech.ipac.visualize.plot.plotdata.FitsRead; +import edu.caltech.ipac.visualize.plot.plotdata.FitsReadUtil; +import nom.tam.fits.FitsException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @author Trey Roby + * + */ +public class FluxValueUtil { + + private static final List unavailable= asResList(PixelValue.Result.makeUnavailable()); + private static final List nan= asResList(new PixelValue.Result(PixelValue.Result.STATUS_NAN,"","","")); + private static final List retrieving= asResList(PixelValue.Result.makeRetrieving()); + + private static List asResList(PixelValue.Result v) { return Collections.singletonList(v); } + + private static boolean isValidCubeMatch(FitsRead[] frAry, int cubePlane) { + if (frAry==null || frAry.length==0) return false; + return frAry[0].isCube() + && frAry.length >= cubePlane + && frAry[cubePlane].getPlaneNumber() == cubePlane + && frAry[0].getHduNumber() == frAry[cubePlane].getHduNumber(); + } + + public static List getFluxHiPS(String fitsTileUrl, ImagePt pt, WorldPt wpt, int cubePlane) { + if (HiPSRetrieve.isHiPSFileCached(fitsTileUrl)) { + File fitsFile= HiPSRetrieve.getHipsCacheFile(URLDownload.makeURL(fitsTileUrl),null); + try { + var frAry = FitsCacher.readFits(fitsFile).getFitReadAry(); + var cube= frAry[0].isCube(); + if (cube && !isValidCubeMatch(frAry, cubePlane)) return unavailable; + var units= FitsReadUtil.getBUnit(frAry[0].getHeader(),"---"); + var ipt= cube ? ImagePlot.getImageCoords(frAry[cubePlane],wpt) : pt; + var val= frAry[cubePlane].getFlux(ipt); + var result= new PixelValue.Result( + PixelValue.Result.STATUS_VALUE, PixelValue.Result.TYPE_FLOAT,val+"","", units); + return asResList(result); + } catch (IOException | FitsException | ProjectionException e) { + return unavailable; + } + } + else { + if (HiPSRetrieve.isDeadFitsUrl(fitsTileUrl)) return nan; + HiPSRetrieve.retrieveHiPSTileInBackground(fitsTileUrl); + return retrieving; + } + } + + public static List getFlux(PlotState[] stateAry, ImagePt ipt) { + if (stateAry == null || stateAry.length == 0) return Collections.emptyList(); + PlotState primState= stateAry[0]; + + // 1. handle primary plot + var faHList = Arrays.stream(primState.getBands()).map(primState::getFileAndHeaderInfo).toList(); + + try { + CtxControl.confirmFiles(stateAry[0]); + } catch (FailedRequestException e) { + return Collections.nCopies(faHList.size(), PixelValue.Result.makeUnavailable()); + } + + var baseList= getFileFlux(faHList, ipt); + if (stateAry.length==1) return baseList; + + // 2. if there are overlays - handle them + List fluxList= new ArrayList<>(baseList); + for(int i=1; (i getFileFlux(List fileAndHeader, ImagePt ipt) { + return fileAndHeader.stream() + .map (fap -> PixelValue.getPixelValue(ServerContext.convertToFile(fap.fileName()), ipt, fap.header())) + .toList(); + } +} diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/ImagePlotBuilder.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/ImagePlotBuilder.java index 1c84d7c09a..01ce040dd6 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/ImagePlotBuilder.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/ImagePlotBuilder.java @@ -28,6 +28,7 @@ import java.util.Map; import static edu.caltech.ipac.firefly.server.visualize.ProgressStat.PType; +import static edu.caltech.ipac.firefly.server.visualize.WebPlotFactory.makePlottingException; import static edu.caltech.ipac.firefly.visualize.Band.BLUE; import static edu.caltech.ipac.firefly.visualize.Band.GREEN; import static edu.caltech.ipac.firefly.visualize.Band.NO_BAND; @@ -73,7 +74,7 @@ public static PlotInfo create3Color(WebPlotRequest redRequest, if (piAry!=null && piAry.length>0) retval= piAry[0]; return retval; } catch (Exception e) { - throw makeException(e); + throw makePlottingException(e); } } @@ -88,22 +89,10 @@ private static List createList(WebPlotRequest wpr) throws FailedReques Collections.addAll(retList, allPlots.plotInfoAry()); return retList; } catch (Exception e) { - throw makeException(e); + throw makePlottingException(e); } } - private static FailedRequestException makeException(Exception e) { - if (e instanceof FailedRequestException) { - return new FailedRequestException("Could not create plot. " + e.getMessage(), - ((FailedRequestException)e).getDetailMessage()); - } - else if (e instanceof FitsException) { - return new FailedRequestException("Could not create plot. Invalid FITS File format.", e.getMessage()); - } - else { - return new FailedRequestException("Could not create plot.", e.getMessage(), e); - } - } static Results build(Map requestMap, MultiImageAction multiAction, @@ -162,7 +151,13 @@ private static Map findFiles(Map requestMa WebPlotRequest request = entry.getValue(); FileRetriever retrieve = ImageFileRetrieverFactory.getRetriever(request); if (retrieve != null) { - fitsFiles.put(band, retrieve.getFile(request)); + FileInfo fi= retrieve.getFile(request); + if (fi.isSuccess()) { + fitsFiles.put(band, retrieve.getFile(request)); + } + else { + throw new FailedRequestException(fi.getResponseCodeMsg(),"",fi.getResponseCode(), fi); + } } else { _log.error("failed to find FileRetriever should only be FILE, URL, ALL_SKY, or SERVICE, for band " + band.toString()); } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisJsonSerializer.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisJsonSerializer.java index 0014b96ef2..1184592a0f 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisJsonSerializer.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisJsonSerializer.java @@ -65,6 +65,7 @@ private static JSONObject createJsonPixelResultObj(PixelValue.Result r) { putStrNotNull(jsonPixelResult,"type",r.type()); putStrNotNull(jsonPixelResult,"valueBase10",r.valueBase10()); putStrNotNull(jsonPixelResult,"valueBase16",r.valueBase16()); + putStrNotNull(jsonPixelResult,"unit",r.unit()); return jsonPixelResult; } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java index 8468507bf5..9be89ec57c 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/VisServerOps.java @@ -32,7 +32,6 @@ import edu.caltech.ipac.visualize.plot.CropFile; import edu.caltech.ipac.visualize.plot.Histogram; import edu.caltech.ipac.visualize.plot.ImagePt; -import edu.caltech.ipac.visualize.plot.PixelValue; import edu.caltech.ipac.visualize.plot.plotdata.FitsExtract; import edu.caltech.ipac.visualize.plot.plotdata.FitsRead; import edu.caltech.ipac.visualize.plot.plotdata.FitsReadUtil; @@ -74,6 +73,7 @@ public class VisServerOps { private static final int BYTE_STRETCH_MAX_ACCESS_CNT = 1; private static final String BYTE_STRETCH_SEMAPHORE_KEY = "BYTE_STRETCH_SEMAPHORE"; + /** * create a new 3 color plot * @return PlotCreationResult the results @@ -163,35 +163,6 @@ public static List getPointDataAry(PlotState state, ImagePt[] ptAry, int (f) -> FitsExtract.getPointDataAryFromFile(ptAry, plane, f, hduNum, ptSizeX, ptSizeY, ct)); } - public static List getFlux(PlotState[] stateAry, ImagePt ipt) { - PlotState primState= stateAry[0]; - - // 1. handle primary plot - var faHList = Arrays.stream(primState.getBands()).map(primState::getFileAndHeaderInfo).toList(); - - try { - CtxControl.confirmFiles(stateAry[0]); - } catch (FailedRequestException e) { - return faHList.stream().map( f -> PixelValue.Result.makeUnavailable()).toList(); - } - - var baseList= getFileFlux(faHList, ipt); - if (stateAry.length==1) return baseList; - - // 2. if there are overlays - handle them - List fluxList= new ArrayList<>(baseList); - for(int i=1; (i getFileFlux(List fileAndHeader, ImagePt ipt) { - return fileAndHeader.stream() - .map (fap -> PixelValue.getPixelValue(ServerContext.convertToFile(fap.fileName()), ipt, fap.header())) - .toList(); - } private static Semaphore getUserSemaphore() { Cache cache= CacheManager.getSessionCache(); diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/WebPlotFactory.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/WebPlotFactory.java index cbb163da5e..05ccf80668 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/WebPlotFactory.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/WebPlotFactory.java @@ -103,13 +103,18 @@ private static WebPlotFactoryRet create(Map requestMap, return new WebPlotFactoryRet(wpInit,wpHeader); } catch (Exception e) { PlotServUtils.updateProgress(saveRequest, PType.FAIL, "Failed"); - throw makeException(e); + throw makePlottingException(e); } } - private static FailedRequestException makeException(Exception e) { - if (e instanceof FailedRequestException) return (FailedRequestException)e; - else if (e instanceof FitsException) return new FailedRequestException(e.getMessage(), e.getMessage(), e); + public static FailedRequestException makePlottingException(Exception e) { + if (e instanceof FailedRequestException fe) { + var msg= fe.getResponseCode()>0 + ? String.format("Could not create plot. %s (%d)", fe.getMessage(), fe.getResponseCode()) + : String.format("Could not create plot. %s", fe.getMessage()); + return new FailedRequestException(msg, fe.getDetailMessage(),fe); + } + else if (e instanceof FitsException) return new FailedRequestException("Could not create plot. FITS reading Failed: "+e.getMessage(), e.getMessage(), e); else return new FailedRequestException("Could not create plot.", e.getMessage(), e); } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSListUtil.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSListUtil.java index 5538ea7fbb..9857341d3f 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSListUtil.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSListUtil.java @@ -3,12 +3,17 @@ import edu.caltech.ipac.firefly.data.FileInfo; import edu.caltech.ipac.firefly.server.servlets.HiPSRetrieve; import edu.caltech.ipac.firefly.server.util.Logger; +import edu.caltech.ipac.table.DataGroup; +import edu.caltech.ipac.table.DataObject; +import edu.caltech.ipac.table.TableUtil; +import edu.caltech.ipac.util.download.URLDownload; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.StringReader; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -21,6 +26,7 @@ public class HiPSListUtil { static final Logger.LoggerImpl _log = Logger.getLogger(); private final static String PROP = HiPSMasterListEntry.PARAMS.PROPERTIES.getKey().toLowerCase(); + static final Logger.LoggerImpl log = Logger.getLogger(); private final static Map defParamMap = new HashMap<>(); @@ -68,8 +74,10 @@ public static List createHiPSListFromUrl(String url, String _log.debug("executing " + source + " url query: " + url); long cTime = System.currentTimeMillis(); + String hipsListName= "hips-list.properties"; + String fileName= childExt!=null? childExt+"-"+hipsListName : hipsListName; - FileInfo listFileInfo = HiPSRetrieve.retrieveHiPSData(url, childExt, false); + FileInfo listFileInfo = HiPSRetrieve.retrieveHiPSData(url, fileName, false); File listFile= listFileInfo.getFile(); if (listFile==null) throw new IOException("Could not retrieve file: "+ listFileInfo.getResponseCode()); @@ -181,7 +189,7 @@ private static void addItemsFromProperties(HiPSMasterListEntry listEntry, Map getAdditionalMOCS(String urlStr, String source) { + try { + if (urlStr==null) return null; + URL url= URLDownload.makeURL(urlStr); + if (url == null) return null; + FileInfo listFileInfo = HiPSRetrieve.retrieveHiPSData(url.toString(), source+"-moc-table.dat", false); + if (listFileInfo.getResponseCode()!=200 && listFileInfo.getResponseCode()!=304) return null; + DataGroup dg= TableUtil.readAnyFormat(listFileInfo.getFile()); + if (dg == null || dg.isEmpty()) return null; + var retList = new ArrayList(); + String baseUrl= null; + if (url.getQuery()==null) { + String s= urlStr.endsWith("/") ? urlStr.substring(0,urlStr.length()-1) : urlStr; + var endIdx= s.lastIndexOf("/"); + baseUrl= endIdx>-1 ? s.substring(0, s.lastIndexOf("/")) : s; + } + + for(var i=0; i getHiPSListData(String[] dataTypes, String sour } public List getAdditionalMOCS(String source) { - try { - URL url= new URI(getMocUrl()).toURL(); - FileInfo listFileInfo = HiPSRetrieve.retrieveHiPSData(url.toString(), source, false); - if (listFileInfo.getResponseCode()!=200 && listFileInfo.getResponseCode()!=304) return null; - DataGroup dg= TableUtil.readAnyFormat(listFileInfo.getFile()); - var retList = new ArrayList(); - - for(var i=0; i getHiPSListData(String[] dataTypes, String source) { @@ -37,57 +27,9 @@ public List getHiPSListData(String[] dataTypes, String sour } public List getAdditionalMOCS(String source) { - try { - if (lsstMocListUrl==null) return null; - URL url= URLDownload.makeURL(lsstMocListUrl); - if (url == null) return null; - FileInfo listFileInfo = HiPSRetrieve.retrieveHiPSData(url.toString(), source, false); - if (listFileInfo.getResponseCode()!=200 && listFileInfo.getResponseCode()!=304) return null; - DataGroup dg= TableUtil.readAnyFormat(listFileInfo.getFile()); - if (dg == null || dg.isEmpty()) return null; - var retList = new ArrayList(); - String baseUrl= null; - if (url.getQuery()==null) { - String s= lsstMocListUrl.endsWith("/") ? lsstMocListUrl.substring(0,lsstMocListUrl.length()-1) : lsstMocListUrl; - var endIdx= s.lastIndexOf("/"); - baseUrl= endIdx>-1 ? s.substring(0, s.lastIndexOf("/")) : s; - } - - for(var i=0; i 1 && StringUtils.getInt(npixNumParts[0], -1) > -1; + } + return false; + } + private static UriRef makeS3UriRef(WebPlotRequest request) { var region= request.getS3Region(); var bucket= request.getS3Bucket(); diff --git a/src/firefly/java/edu/caltech/ipac/util/FitsHDUUtil.java b/src/firefly/java/edu/caltech/ipac/util/FitsHDUUtil.java index e603506818..b7793c7605 100644 --- a/src/firefly/java/edu/caltech/ipac/util/FitsHDUUtil.java +++ b/src/firefly/java/edu/caltech/ipac/util/FitsHDUUtil.java @@ -34,7 +34,7 @@ import static edu.caltech.ipac.firefly.core.FileAnalysisReport.Type.Image; import static edu.caltech.ipac.firefly.core.FileAnalysisReport.Type.Table; import static edu.caltech.ipac.visualize.plot.plotdata.FitsReadUtil.getAxisCnt; -import static edu.caltech.ipac.visualize.plot.plotdata.FitsReadUtil.getNaxisLength; +import static edu.caltech.ipac.visualize.plot.plotdata.FitsReadUtil.getNaxisN; /** @@ -99,11 +99,11 @@ public static FitsAnalysisReport analyze(File infile, FileAnalysisReport.ReportT int axisCnt= (workingAxis != 4) ? workingAxis - : getNaxisLength(header, 4, isCompressed)==1 ? 3 :workingAxis; + : getNaxisN(header, 4, isCompressed)==1 ? 3 :workingAxis; desc.append((axisCnt <= 2) ? " (" : axisCnt == 3 ? " (cube " : " (" + axisCnt + "d "); if (axisCnt>1) { for(int d=1;(d<=axisCnt);d++) { - desc.append(String.format("%d", getNaxisLength(header,d,isCompressed))); + desc.append(String.format("%d", getNaxisN(header,d,isCompressed))); desc.append(d < axisCnt ? " x " : ")"); } if (axisCnt>=4) { @@ -113,7 +113,7 @@ public static FitsAnalysisReport analyze(File infile, FileAnalysisReport.ReportT } } else { - desc.append(String.format("%d x 1)", getNaxisLength(header, 1, isCompressed))); + desc.append(String.format("%d x 1)", getNaxisN(header, 1, isCompressed))); } part.setDesc(desc.toString()); diff --git a/src/firefly/java/edu/caltech/ipac/util/FormatUtil.java b/src/firefly/java/edu/caltech/ipac/util/FormatUtil.java index 335a46de24..43d718f926 100644 --- a/src/firefly/java/edu/caltech/ipac/util/FormatUtil.java +++ b/src/firefly/java/edu/caltech/ipac/util/FormatUtil.java @@ -61,6 +61,7 @@ public enum Format { REGION ("reg", ".reg", "application/region-file"), PNG ("png", ".png", "image/png"), JPEG ("jpeg", ".jpg", "image/jpeg"), + WEBP ("webp", ".webp", "image/webp"), UWS ("uws", ".xml", "application/xml+uws"), PARQUET (DuckDbReadable.Parquet.NAME, "."+DuckDbReadable.Parquet.NAME, "application/vnd.apache.parquet"), ZIP ("zip", ".zip", "application/zip"), @@ -99,6 +100,8 @@ public static Format fromMime(String mime) { case "application/csv" -> CSV; case "application/tsv" -> TSV; case "image/jpg" -> JPEG; + case "image/png" -> PNG; + case "image/webp" -> WEBP; case "application/x-zip-compressed" -> ZIP; case "application/x-gzip" -> GZIP; case "application/tar" -> TAR; @@ -134,6 +137,9 @@ public static MimeDesc getMimeType(String inFile) { // JPEG: FF D8 FF if (magic(hdr, 0, 0xFF,0xD8,0xFF)) return new MimeDesc(JPEG.mime(), "JPEG image"); + // WEBP RIFF, 32 bit int with file size, WEBP + if (magic(hdr, 0, 'R','I','F','F') && magic(hdr, 8, 'W','E','B','P')) + return new MimeDesc(WEBP.mime(), "WEBP image"); // PDF: %PDF if (magic(hdr, 0, '%','P','D','F')) return new MimeDesc(PDF.mime(), "PDF document"); diff --git a/src/firefly/java/edu/caltech/ipac/util/download/RetrieveUtil.java b/src/firefly/java/edu/caltech/ipac/util/download/RetrieveUtil.java index 38e8bb6da0..0558184b09 100644 --- a/src/firefly/java/edu/caltech/ipac/util/download/RetrieveUtil.java +++ b/src/firefly/java/edu/caltech/ipac/util/download/RetrieveUtil.java @@ -39,11 +39,11 @@ public class RetrieveUtil { * @throws FailedRequestException when request fails */ public static FileInfo downloadCaching(UriRefParams params, DownloadListener dl) throws FailedRequestException { - if (params==null) throw new FailedRequestException("downloadCaching: params is null"); + if (params==null || params.getUriRef()==null) { + throw new FailedRequestException("downloadCaching: params or uri is null"); + } FileInfo fileInfo= FileCacheHelper.getFileInfo(params); if (fileInfo!=null && !params.getCheckForNewer()) return fileInfo; - - try { File fileName= (fileInfo==null) ? FileCacheHelper.makeFile(params.getDownloadDir(), params.getUniqueString()) : fileInfo.getFile(); var ops= URLDownload.Options.def(); diff --git a/src/firefly/java/edu/caltech/ipac/util/download/UriRefParams.java b/src/firefly/java/edu/caltech/ipac/util/download/UriRefParams.java index 56f7d0da4f..bde5d9b812 100644 --- a/src/firefly/java/edu/caltech/ipac/util/download/UriRefParams.java +++ b/src/firefly/java/edu/caltech/ipac/util/download/UriRefParams.java @@ -23,8 +23,15 @@ public class UriRefParams extends BaseNetParams { private HttpServiceInput addtlInfo; private boolean expectStaticFile= false; + public UriRefParams(String urlStr) { + this(Collections.singletonList(UriRef.make(urlStr)),null,null); + setStatusKey(urlStr); + } - public UriRefParams(URL url) { this(Collections.singletonList(UriRef.make(url)),null,null); } + public UriRefParams(URL url) { + this(Collections.singletonList(UriRef.make(url)),null,null); + setStatusKey(url.toString()); + } public UriRefParams(UriRef ref, File downloadDir) { this(Collections.singletonList(ref),null,null); diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/ImagePlot.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/ImagePlot.java index e8633f7010..2d26e65df0 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/ImagePlot.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/ImagePlot.java @@ -21,6 +21,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static edu.caltech.ipac.firefly.visualize.Band.NO_BAND; + /** * This class is now only used for creating PNG file, not for interactive visualization * @author Trey Roby @@ -65,7 +67,6 @@ private ImagePlot(ActiveFitsReadGroup frGroup, boolean threeColor, int initColor configureImage(); } - /** 07/20/15 LZ * Create a ImagePlot with given IndexColorModel */ @@ -82,6 +83,12 @@ public ImagePlot(ActiveFitsReadGroup frGroup, ImageMask[] iMasks) throws FitsEx configureImage(); } + public static ImagePlot makeImagePlot(FitsRead fr) { + var frGroup= new ActiveFitsReadGroup(); + frGroup.setFitsRead(NO_BAND,fr); + return new ImagePlot(frGroup, false); + } + public boolean isUseForMask() { return useForMask; } public PlotGroup getPlotGroup() { return plotGroup; } public void setPlotDesc(String d) { plotDesc = d; } @@ -262,6 +269,11 @@ else if (wpt.getCoordSys().equals(CoordinateSys.PIXEL)) { } } + public static ImagePt getImageCoords(FitsRead fr, WorldPt wpt) throws ProjectionException { + var imWp= ImagePlot.makeImagePlot(fr).getImageCoords(wpt); + return new ImagePt(imWp.getX(),imWp.getY()); + } + public ImageWorkSpacePt getImageWorkSpaceCoords(Point2D pt) throws NoninvertibleTransformException { AffineTransform inverse= plotGroup.getInverseTransform(); @@ -336,7 +348,7 @@ public WorldPt getWorldCoords(Point2D pt) throws NoninvertibleTransformException } - /** + /** * Return a point the represents the passed point with a distance in * World coordinates added to it. * @param pt the x and y coordinate @@ -406,6 +418,17 @@ public WorldPt getWorldCoords( ImageWorkSpacePt ipt, CoordinateSys outputCoordSy return wpt; } + + public WorldPt getWorldCoords(FitsRead fr, ImagePt ipt, CoordinateSys outputCoordSys) { + var plot= ImagePlot.makeImagePlot(fr); + try { + return plot.getWorldCoords(new ImageWorkSpacePt(ipt.getX(), ipt.getY())); + } catch (ProjectionException ignore) { + return null; + } + } + + /** * get the scale (usually in arcseconds) that on image pixel of data * represents. diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/PixelValue.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/PixelValue.java index 9fea787c22..c3f56fc03a 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/PixelValue.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/PixelValue.java @@ -183,8 +183,12 @@ private static long readValueLong(RandomAccessFile fits_file, long file_pointer) return fits_file.readLong(); } - public record Result(String status, String type, String valueBase10, String valueBase16) { + public record Result(String status, String type, String valueBase10, String valueBase16, String unit) { + public Result(String status, String type, String valueBase10, String valueBase16) { + this (status, type, valueBase10, valueBase16, null); + } public static final String STATUS_UNAVAILABLE= "UNAVAILABLE"; + public static final String STATUS_RETRIEVING= "RETRIEVING"; public static final String STATUS_NAN= "NaN"; public static final String STATUS_UNDEFINED= "UNDEFINED"; public static final String STATUS_VALUE= "VALUE"; @@ -192,6 +196,8 @@ public record Result(String status, String type, String valueBase10, String valu public static final String TYPE_DECIMAL_INT = "DECIMAL_INT"; public static final String TYPE_FLOAT= "FLOAT"; private static final Result unavailable= new Result(STATUS_UNAVAILABLE,TYPE_EMPTY,"",""); + private static final Result retrieving= new Result(STATUS_RETRIEVING,TYPE_EMPTY,"",""); public static Result makeUnavailable() {return unavailable;} + public static Result makeRetrieving() {return retrieving;} }; } diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsExtract.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsExtract.java index d3832bf169..47c5f07c2c 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsExtract.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsExtract.java @@ -408,7 +408,7 @@ private static void validateCubeAtHDU(BasicHDU[] hdus, int hduNum) throws Fit String hduNumStr= "HDU #"+hduNum; int nAxis= FitsReadUtil.getNaxis(header); if (nAxis<3) throw new FitsException(hduNumStr + " is not a cube"); - if (nAxis==4 && FitsReadUtil.getNaxisLength(header,4)!=1) throw new FitsException(hduNumStr + " is not a cube, 4 axes"); + if (nAxis==4 && FitsReadUtil.getNaxisN(header,4)!=1) throw new FitsException(hduNumStr + " is not a cube, 4 axes"); } private static void validateImageAtHDU(BasicHDU[] hdus, int hduNum) throws FitsException { diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java index 01209e0a4a..7ca6559da7 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsRead.java @@ -99,7 +99,7 @@ private static ImageHDU makeImageHDU(BasicHDU hdu) throws FitsException { public int getNaxis() { return FitsReadUtil.getNaxis(header); } public int getNaxis1() { return FitsReadUtil.getNaxis1(header); } public int getNaxis2() { return FitsReadUtil.getNaxis2(header); } - public int getNaxisLength(int axis) { return FitsReadUtil.getNaxisLength(header, axis); } + public int getNaxisLength(int axis) { return FitsReadUtil.getNaxisN(header, axis); } public String getBUnit() { return this.bunit;} public double getBscale() { return FitsReadUtil.getBscale(header); } public double getBzero() { return FitsReadUtil.getBzero(header); } diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsReadUtil.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsReadUtil.java index 2fceb04c89..f29a408134 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsReadUtil.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/plotdata/FitsReadUtil.java @@ -22,6 +22,7 @@ import nom.tam.fits.UndefinedData; import nom.tam.fits.UndefinedHDU; import nom.tam.fits.header.Bitpix; +import nom.tam.fits.header.Standard; import nom.tam.image.StandardImageTiler; import nom.tam.image.compression.hdu.CompressedImageHDU; import nom.tam.util.ArrayFuncs; @@ -46,8 +47,7 @@ public class FitsReadUtil { public static final String SPOT_OFF = "SPOT_OFF"; public static final String SPOT_BP = "SPOT_BP"; // original bitpix public static final String SPOT_PL = "SPOT_PL"; // cube plane number, only used with cubes, deprecated - public static final String EXTNAME= "EXTNAME"; - public static final String EXTTYPE= "EXTTYPE"; + private static final String EXTTYPE= "EXTTYPE"; private static final String RESTORE_COMMENT= "restored after uncompression by Firefly"; public static ImageData getImageData(BasicHDU refHdu, float[] float1d) { @@ -107,7 +107,7 @@ public static UncompressFitsInfo createdUncompressVersionOfFile(BasicHDU[] HD String extType= getExtType(header,null); ImageHDU iHDU= cHDU.asImageHDU(); var h= iHDU.getHeader(); - if (getExtName(h)==null && extName!=null) h.addValue(EXTNAME,extName, RESTORE_COMMENT); + if (getExtName(h)==null && extName!=null) h.addValue(Standard.EXTNAME.key(),extName, RESTORE_COMMENT); if (getExtType(h,null)==null && extType!=null) h.addValue(EXTTYPE,extType, RESTORE_COMMENT); fits.addHDU(iHDU); } @@ -415,38 +415,35 @@ public static void writeFitsFileForCropOnly(File outfile, FitsRead[] fitsReadAry outputFits.write(outfile); } - public static int getBitPix(Header h) {return h.getIntValue("BITPIX"); } - public static int getNaxis(Header h) { return h.getIntValue("NAXIS", 0); } + public static int getBitPix(Header h) {return h.getIntValue(Standard.BITPIX); } + public static int getNaxis(Header h) { return h.getIntValue(Standard.NAXIS, 0); } public static int getZNaxis(Header h) { return h.getIntValue("ZNAXIS", 0); } - public static int getNaxis1(Header h) { return h.getIntValue("NAXIS1", 0); } - public static int getNaxis2(Header h) { return h.getIntValue("NAXIS2", 0); } - public static int getNaxis3(Header h) { return (getNaxis2(h) > 1) ? h.getIntValue("NAXIS3") : 1; } - - public static int getNaxisLength(Header h, int num) { - if (num<1) return 0; - return h.getIntValue("NAXIS"+num,0); - } + public static int getNaxis1(Header h) { return getNaxisN(h,1); } + public static int getNaxis2(Header h) { return getNaxisN(h,2); } + public static int getNaxis3(Header h) { return (getNaxis2(h) > 1) ? getNaxisN(h,3) : 1; } + public static int getNaxisN(Header h, int num) { return num>0 ? h.getIntValue(Standard.NAXISn.n(num),0) : 0; } public static int getAxisCnt(Header h, boolean compressed) { if (!compressed) getNaxis(h); return getZNaxis(h)==0 ? getNaxis(h) : getZNaxis(h); } - public static int getNaxisLength(Header h, int num, boolean compressed) { - if (!compressed) return getNaxisLength(h, num); - return getZNaxisLength(h,num) > -1 ? getZNaxisLength(h,num) : getNaxisLength(h,num); + public static int getNaxisN(Header h, int num, boolean compressed) { + if (!compressed) return getNaxisN(h, num); + return getZNaxisN(h,num) > -1 ? getZNaxisN(h,num) : getNaxisN(h,num); } - public static int getZNaxisLength(Header h, int axis) { return h.getIntValue("ZNAXIS"+axis,-1); } - public static double getBscale(Header h) { return h.getDoubleValue("BSCALE", 1.0); } - public static double getBzero(Header h) { return h.getDoubleValue("BZERO", 0.0); } + public static int getZNaxisN(Header h, int axis) { return h.getIntValue("ZNAXIS"+axis,-1); } + public static double getBscale(Header h) { return h.getDoubleValue(Standard.BSCALE, 1.0); } + public static double getBzero(Header h) { return h.getDoubleValue(Standard.BZERO, 0.0); } public static double getBlankValue(Header h) { // blank value is only applicable to integer values (BITPIX > 0) - return getBitPix(h) > 0 ? h.getDoubleValue("BLANK", Double.NaN) : Double.NaN; + return getBitPix(h) > 0 ? h.getDoubleValue(Standard.BLANK, Double.NaN) : Double.NaN; } - public static String getBUnit(Header h) { return h.getStringValue("BUNIT", ""); } - public static String getExtName(Header h) { return h.getStringValue(EXTNAME); } + public static String getBUnit(Header h) { return getBUnit(h,""); } + public static String getBUnit(Header h, String def) { return h.getStringValue(Standard.BUNIT, def); } + public static String getExtName(Header h) { return h.getStringValue(Standard.EXTNAME); } public static String getExtType(Header h, String defVal) { return h.getStringValue(EXTTYPE,defVal); } public static String getUtype(Header h) { return h.getStringValue("UTYPE"); } public static String getExtNameOrType(Header h) { return getExtName(h)!=null ? getExtName(h) : getExtType(h,null);} @@ -496,7 +493,7 @@ public static void closeFits(Fits fits) { public static Object dataArrayFromFitsFile(ImageHDU hdu, int x, int y, int width, int height, int plane, Class arrayType) throws IOException { Header header= hdu.getHeader(); int naxis= getNaxis(header); - if (naxis==4 && getNaxisLength(header,4)!=1) throw new IllegalArgumentException("naxis 4 must be only 1 dimension"); + if (naxis==4 && getNaxisN(header,4)!=1) throw new IllegalArgumentException("naxis 4 must be only 1 dimension"); else if (naxis!=2 && naxis!=3 && naxis!=4) throw new IllegalArgumentException("only naxis 2 or 3 or 4 is supported"); int[] loc= null; int[] tileSize= null; diff --git a/src/firefly/js/core/BootstrapRegistry.js b/src/firefly/js/core/BootstrapRegistry.js index 2515837ddb..7cdd23f3fe 100644 --- a/src/firefly/js/core/BootstrapRegistry.js +++ b/src/firefly/js/core/BootstrapRegistry.js @@ -35,6 +35,7 @@ import ExtractLineTool from '../drawingLayers/ExtractLineTool.js'; import PointSelection from '../drawingLayers/PointSelection.js'; import ExtractPoints from '../drawingLayers/ExtractPointsTool.js'; import SearchSelectTool from '../drawingLayers/SearchSelectTool.js'; +import ExtractHiPSTileTool from '../drawingLayers/ExtractHiPSTileTool'; import StatsPoint from '../drawingLayers/StatsPoint.js'; import NorthUpCompass from '../drawingLayers/NorthUpCompass.js'; import ImageRoot from '../drawingLayers/ImageRoot.js'; @@ -113,7 +114,7 @@ export const getBootstrapRegistry= once(() => { }; const drawLayerFactory= DrawLayerFactory.makeFactory( - FixedMarker, SelectArea,DistanceTool, ExtractLineTool, ExtractPoints, + FixedMarker, SelectArea,DistanceTool, ExtractLineTool, ExtractPoints, ExtractHiPSTileTool, PointSelection, StatsPoint, NorthUpCompass, ImageRoot, SearchTarget, Catalog, HpxCatalog, Artifact, WebGrid, RegionPlot, MarkerTool, FootprintTool, SearchSelectTool, HiPSGrid, HiPSMOC, ImageOutline, ImageLineBasedFootprint); diff --git a/src/firefly/js/data/ServerParams.js b/src/firefly/js/data/ServerParams.js index d6bb72e04d..7e35bd7dab 100644 --- a/src/firefly/js/data/ServerParams.js +++ b/src/firefly/js/data/ServerParams.js @@ -90,6 +90,7 @@ export const ServerParams = { LSST : 'lsst', ALL : 'all', CDS : 'cds', + IS_HIPS_TILE : 'isHipsTile', HIPS_SOURCES : 'hipsSources', HIPS_LIST_SOURCE: 'hipsListSource', HIPS_LIST_SOURCE_NAME: 'hipsListSourceName', diff --git a/src/firefly/js/drawingLayers/ExtractHiPSTileTool.js b/src/firefly/js/drawingLayers/ExtractHiPSTileTool.js new file mode 100644 index 0000000000..c2a333bccd --- /dev/null +++ b/src/firefly/js/drawingLayers/ExtractHiPSTileTool.js @@ -0,0 +1,203 @@ +/* + * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt + */ + + +import {visRoot} from '../api/ApiUtilImage'; +import CsysConverter from '../visualize/CsysConverter'; +import ShapeDataObj from '../visualize/draw/ShapeDataObj'; +import {dispatchForceDrawLayerUpdate} from '../visualize/DrawLayerDispatch'; +import {dispatchAttributeChange} from '../visualize/ImagePlotDispatch'; +import {PlotAttribute} from '../visualize/PlotAttribute'; +import {isDrawLayerVisible, currentP, getCenterOfProjection, refreshP} from '../visualize/PlotViewUtil.js'; +import {getHiPSNorderlevel, getHealpixCellAtNorder, extractFitsFromHiPS} from '../visualize/HiPSUtil.js'; +import FootprintObj from '../visualize/draw/FootprintObj.js'; +import {makeDrawingDef} from '../visualize/draw/DrawingDef.js'; +import DrawLayer, {ColorChangeType} from '../visualize/draw/DrawLayer.js'; +import {makeFactoryDef} from '../visualize/draw/DrawLayerFactory.js'; +import CysConverter from '../visualize/CsysConverter'; +import {getAllPlotViewIdByOverlayLock} from '../visualize/PlotViewUtil'; +import {isDefined} from '../util/WebUtil.js'; +import {changeHiPSProjectionCenterAndType, isHiPSAitoff} from 'firefly/visualize/WebPlot.js'; +import {makeImagePt, pointEquals} from '../visualize/Point'; +import { + ANY_REPLOT, ATTACH_LAYER_TO_PLOT, CHANGE_CENTER_OF_PROJECTION, CHANGE_VISIBILITY, FORCE_DRAW_LAYER_UPDATE, + MODIFY_CUSTOM_FIELD, SELECT_POINT +} from '../visualize/VisConst'; +import {MouseState} from '../visualize/VisMouseSync'; + +const ID= 'EXTRACT_HIPS_FILE_TOOL'; +const TYPE_ID= `${ID}_TYPE`; + + +let lastDownClick= undefined; + +const factoryDef= makeFactoryDef(TYPE_ID,creator,null,getLayerChanges,null,undefined); + +export default {factoryDef, TYPE_ID}; // every draw layer must default export with factoryDef and TYPE_ID + +let idCnt=0; + +function getTargetOrders(plot) { + const maxOrder = Number(plot.hipsProperties?.hips_order); + const {norder} = getHiPSNorderlevel(plot, true); + return { + targetNorder: maxOrder > 6 ? maxOrder - 1 : maxOrder, + maxOrder, + norder, + }; +} + +function dispatchSelectPoint(mouseStatePayload) { + const {plotId,screenPt,drawLayer}= mouseStatePayload; + if (mouseStatePayload.shiftDown || !drawLayer.drawData.data) return; + let {plot}= currentP(plotId); + if (!plot?.hasFits) return; + const center= getCenterOfProjection(plot); + if (lastDownClick && (!pointEquals(center, lastDownClick?.center) || lastDownClick.plotId!==plotId)) return; + + setTimeout(() => { + plot= refreshP(plot); + if (!plot) return; + const cc= CsysConverter.make(currentP(plotId).plot); + const pt= cc.getWorldCoords(screenPt); + if (!pt) return; + + const {maxOrder,norder}= getTargetOrders(plot); + const orderToUse = plot.hasFitsCube ? maxOrder : norder; + const cell = getHealpixCellAtNorder(orderToUse, pt, plot.dataCoordSys); + const oldCell= plot.attributes[PlotAttribute.ACTIVE_HIPS_CELL] ?? {}; + if (oldCell.ipix!==cell.ipix || oldCell.norder!==cell.norder) { + dispatchAttributeChange( {plotId, + changes: { + [PlotAttribute.ACTIVE_HIPS_CELL]: cell, + [PlotAttribute.ACTIVE_HIPS_NORDER]: orderToUse, + }}); + dispatchForceDrawLayerUpdate(drawLayer.drawLayerId, plotId); + } + },0); +} + + +function saveLastDown(mouseStatePayload) { + const {plotId}= mouseStatePayload; + lastDownClick= {center:getCenterOfProjection(currentP(plotId).plot), plotId}; +} + +function creator(initPayload, presetDefaults) { + + let drawingDef= makeDrawingDef('magenta', {lineWidth:1, size:6} ); + drawingDef= Object.assign(drawingDef,presetDefaults); + + + idCnt++; + + const pairs= { + [MouseState.UP.key]: dispatchSelectPoint, + [MouseState.DOWN.key]: saveLastDown + }; + const actionTypes= [SELECT_POINT]; + + const options= { + hasPerPlotData:true, + isPointData:false, + canUserChangeColor: ColorChangeType.DYNAMIC, + }; + return DrawLayer.makeDrawLayer(`${ID}-${idCnt}`,TYPE_ID, {}, options, drawingDef, actionTypes, pairs); +} + +function getLayerChanges(drawLayer, action) { + switch (action.type) { + case CHANGE_CENTER_OF_PROJECTION: + case ANY_REPLOT: + case FORCE_DRAW_LAYER_UPDATE: + return {drawData:computeDrawData(drawLayer,action)}; + case ATTACH_LAYER_TO_PLOT: + const {plotId} = action.payload; + let {plotIdAry}= action.payload; + + if (!plotIdAry && !plotId) return null; + plotIdAry = plotIdAry ? plotIdAry : [plotId]; + + const title= Object.assign({},drawLayer.title); + plotIdAry.forEach( (id) => title[id]= getTitle()); + + return {title, drawData:computeDrawData(drawLayer,action,) }; + case MODIFY_CUSTOM_FIELD: + return dealWithMods(drawLayer,action); + case CHANGE_VISIBILITY: + if (action.payload.visible) { + return {drawData:computeDrawData(drawLayer,action, true)}; + } + } + return null; +} + + +function getTitle() { + return 'Extract HiPS Tile'; +} + + +function dealWithMods(drawLayer,action) { + // const {changes}= action.payload; + return null; +} + +function computeDrawData(drawLayer,action, isVisible = false) { + const {payload}= action; + const plotIdAry= payload.plotId ? getAllPlotViewIdByOverlayLock(visRoot(), payload.plotId, false, true) : payload.plotIdAry; + if (plotIdAry) { + const drawData= {data: {...drawLayer.drawData.data}}; + const projectionTypeChange= isDefined(payload.fullSky); + + plotIdAry.forEach( (plotId) => { + if (plotId && (isDrawLayerVisible(drawLayer, plotId) || isVisible)) { + drawData.data[plotId] = computeDrawDataForId(plotId, projectionTypeChange, drawLayer.showLabels); + } else { + drawData.data[plotId] = null; + } + }); + return drawData; + } + else { + return drawLayer.drawData; + } +} + +function computeDrawDataForId(plotId, projectionTypeChange) { + let {plot} = currentP(plotId); + + const cell= plot?.attributes[PlotAttribute.ACTIVE_HIPS_CELL]; + const markedNorder= plot?.attributes[PlotAttribute.ACTIVE_HIPS_NORDER]; + if (!plot?.hasFits || !cell) return undefined; + let aitoff = isHiPSAitoff(plot); + const {maxOrder,norder}= getTargetOrders(plot); + if (isNaN(maxOrder)) return undefined; + + if (projectionTypeChange) { + aitoff = !aitoff; + plot = changeHiPSProjectionCenterAndType(plot, undefined, aitoff); + } + + const cc = CysConverter.make(plot); + const scrCorners = cell.wpCorners.map((corner) => cc.getImageCoords(corner)); + if (scrCorners.some((scrC) => !scrC)) return undefined; + + const s1 = cc.getImageCoords(cell.wpCorners[0]); + const s2 = cc.getImageCoords(cell.wpCorners[2]); + const drawAry = [FootprintObj.make([scrCorners])]; + if (s1 && s2) { + drawAry.push( + ShapeDataObj.makeTextWithOffset( + norder cc.pointInPlot(pt)); - const newPt= makeSelectedPt(screenPt,plotId); - if (newPt) newPtAry.push(newPt); - dispatchAttributeChange( - {plotId, changes:{[PlotAttribute.PT_ARY]:newPtAry} }); - flux.process({type:EXTRACT_POINT, payload:mouseStatePayload} ); - dispatchForceDrawLayerUpdate(drawLayer.drawLayerId, plotId); - } + const {plotId, screenPt, drawLayer, shiftDown} = mouseStatePayload; + if (shiftDown || !drawLayer.drawData.data) return; + const {plot} = currentP(plotId); + const ptAry= plot.attributes[PlotAttribute.PT_ARY] ?? []; + const cc= CsysConverter.make(plot); + const newPtAry= ptAry.filter( (pt) => cc.pointInPlot(pt)); + const newPt= makeSelectedPt(screenPt,plotId); + if (newPt) newPtAry.push(newPt); + dispatchAttributeChange( + {plotId, changes:{[PlotAttribute.PT_ARY]:newPtAry}}); + flux.process({type: EXTRACT_POINT, payload: mouseStatePayload}); + dispatchForceDrawLayerUpdate(drawLayer.drawLayerId, plotId); } diff --git a/src/firefly/js/drawingLayers/PointSelection.js b/src/firefly/js/drawingLayers/PointSelection.js index 05a4015745..ab2b449de5 100644 --- a/src/firefly/js/drawingLayers/PointSelection.js +++ b/src/firefly/js/drawingLayers/PointSelection.js @@ -98,8 +98,7 @@ function getLayerChanges(drawLayer, action) { function makeSelectedPt(screenPt,plotId) { const cc= CsysConverter.make(currentP(plotId).plot); - const selPt= cc.getWorldCoords(screenPt); //todo put back - return selPt ?? cc.getImageCoords(screenPt); + return cc.getWorldCoords(screenPt) ?? cc.getImageCoords(screenPt); } diff --git a/src/firefly/js/rpc/PlotServicesJson.js b/src/firefly/js/rpc/PlotServicesJson.js index 49bd49862f..f5c549a709 100644 --- a/src/firefly/js/rpc/PlotServicesJson.js +++ b/src/firefly/js/rpc/PlotServicesJson.js @@ -67,20 +67,31 @@ export const callGetAreaStatistics= (state, ipt1, ipt2, ipt3, ipt4, areaShape = export function callCrop(stateAry, corner1ImagePt, corner2ImagePt, cropMultiAll) { - const params= makeParamsWithStateAry(stateAry,false, [ - {name:ServerParams.PT1, value: corner1ImagePt.toString()}, - {name:ServerParams.PT2, value: corner2ImagePt.toString()}, - {name:ServerParams.CRO_MULTI_ALL, value: cropMultiAll +''} - ]); + const params= makeParamsWithStateAry(stateAry,false, { + [ServerParams.PT1]: corner1ImagePt.toString(), + [ServerParams.PT2]: corner2ImagePt.toString(), + [ServerParams.CRO_MULTI_ALL]: cropMultiAll +'' + }); return doJsonRequest(ServerParams.CROP, params, true); } -export function callGetFileFlux(stateAry, pt) { - const params = makeParamsWithStateAry(stateAry,true, - [ {name: [ServerParams.PT], value: pt.toString()}]); +export function callGetFileFlux(stateAry, pt, wpt,isHips, hipsTileUrl, hipsPlane=0) { + + const obj= { + [ServerParams.PT]: pt.toString(), + }; + if (isHips) { + obj[ServerParams.WPT]= wpt.toString(); + obj[ServerParams.IS_HIPS_TILE]= true+''; + obj[ServerParams.URL]= hipsTileUrl; + obj[ServerParams.PLANE]= hipsPlane; + } + + const params= makeParamsWithStateAry(stateAry,true, obj ); return doJsonRequest(ServerParams.FILE_FLUX_JSON, params,true); } + async function fetchExtraction(plot, inParams, cmd= ServerParams.FITS_EXTRACTION) { const use64Bit= getBixPix(plot)===-64; const params= {...inParams, [ServerParams.EXTRACTION_FLOAT_SIZE]: use64Bit ? 64 : 32}; @@ -167,11 +178,13 @@ export const saveDS9RegionFile= (regionData) => * @param otherParams * @return {Promise} */ -function makeParamsWithStateAry(stateAry, includeDirectAccessData, otherParams=[]) { - return [ - ...makeStateParamAry(stateAry,includeDirectAccessData), - ...otherParams, - ]; +function makeParamsWithStateAry(stateAry, includeDirectAccessData, otherParams={}) { + const stateObj= stateAry.reduce( (obj, s,idx) => { + obj['state'+idx]= s.toJson(includeDirectAccessData); + return obj; + },{} ); + return { ...stateObj, ...otherParams, + }; } /** @@ -179,8 +192,9 @@ function makeParamsWithStateAry(stateAry, includeDirectAccessData, otherParams=[ * @param {boolean} includeDirectAccessData * @return {Array} */ -function makeStateParamAry(startAry, includeDirectAccessData= true) { - return startAry.map( (s,idx) => { - return {name:'state'+idx, value: s.toJson(includeDirectAccessData) }; - } ); -} \ No newline at end of file +// function makeStateParamAry(startAry, includeDirectAccessData= true) { +// return startAry.map( (s,idx) => { +// return {name:'state'+idx, value: s.toJson(includeDirectAccessData) }; +// } ); +// } + diff --git a/src/firefly/js/ui/HiPSImageSelect.jsx b/src/firefly/js/ui/HiPSImageSelect.jsx index 0c59576b9c..9600994dfa 100644 --- a/src/firefly/js/ui/HiPSImageSelect.jsx +++ b/src/firefly/js/ui/HiPSImageSelect.jsx @@ -111,7 +111,7 @@ export function showHiPSSurveysPopup(pv, moc= false) { } else { const hipsUrl = getHipsUrl(); - if (hipsUrl) return; + if (!hipsUrl) return; const plot = pv ? primePlot(pv) : currentP().plot; moc ? createHiPSMocLayer({ diff --git a/src/firefly/js/ui/tap/DataServicesOptions.js b/src/firefly/js/ui/tap/DataServicesOptions.js index 21a8ceb0fc..f6dde0c423 100644 --- a/src/firefly/js/ui/tap/DataServicesOptions.js +++ b/src/firefly/js/ui/tap/DataServicesOptions.js @@ -2,7 +2,6 @@ import {isObject} from 'lodash'; import {getAppOptions} from '../../core/AppDataCntlr'; import {MetaConst} from '../../data/MetaConst'; import {getMetaEntry, getObjectMetaEntry} from '../../tables/TableUtil'; -import {getServiceMetaOptions} from './SiaUtil'; const dsOps= () => getAppOptions().dataServiceOptions ?? {}; diff --git a/src/firefly/js/visualize/ChangePrime.js b/src/firefly/js/visualize/ChangePrime.js index 0f50d5f4ac..3e43621a8d 100644 --- a/src/firefly/js/visualize/ChangePrime.js +++ b/src/firefly/js/visualize/ChangePrime.js @@ -2,10 +2,15 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {dispatchProcessScroll, dispatchZoom} from './ImagePlotDispatch'; +import {dispatchChangeHiPS, dispatchChangePrimePlot, dispatchProcessScroll, dispatchZoom} from './ImagePlotDispatch'; +import {matchCubePlanes} from './task/WcsMatchTask'; import {IMAGE_PLOT_KEY, UserZoomTypes, ZOOM_IMAGE} from './VisConst'; -import {hasWCSProjection, getPlotViewById, primePlot} from './PlotViewUtil.js'; -import {getPixScaleArcSec, getScreenPixScaleArcSec} from './WebPlot.js'; +import { + hasWCSProjection, getPlotViewById, primePlot, getCubeLength, operateOnOthersInPositionGroup, isCube, + getCubePlaneIdx +} from './PlotViewUtil.js'; +import {visRoot} from './VisStoreRoots'; +import {getPixScaleArcSec, getScreenPixScaleArcSec, isHiPS, isImage} from './WebPlot.js'; import {getZoomLevelForScale} from './ZoomUtil.js'; import {CysConverter} from './CsysConverter.js'; import {makeScreenPt} from './Point.js'; @@ -46,8 +51,8 @@ function getZoomDecision(oldP,newP) { export function changePrime(rawAction, dispatcher, getState) { const {plotId,primeIdx:newPrimeIdx}= rawAction.payload; - const visRoot= getState()[IMAGE_PLOT_KEY]; - const pv= getPlotViewById(visRoot, plotId); + let visRoot= getState()[IMAGE_PLOT_KEY]; + let pv= getPlotViewById(visRoot, plotId); const {plots,primeIdx}= pv; const oldP= plots[primeIdx]; const newP= plots[newPrimeIdx]; @@ -57,6 +62,13 @@ export function changePrime(rawAction, dispatcher, getState) { const scrollToImagePt= cc.getImageCoords(makeScreenPt(pv.scrollX,pv.scrollY)); dispatcher(rawAction); checkZoom(plotId,oldP, newP, scrollToImagePt,visRoot); + + visRoot= getState()[IMAGE_PLOT_KEY]; + pv= getPlotViewById(visRoot, plotId); + const plot= primePlot(pv); + + if (isCube(plot) && visRoot.positionLock) matchCubePlanes(plotId); + } /** @type actionWatcherCallback */ diff --git a/src/firefly/js/visualize/HiPSUtil.js b/src/firefly/js/visualize/HiPSUtil.js index 392d930877..0656d3d23b 100644 --- a/src/firefly/js/visualize/HiPSUtil.js +++ b/src/firefly/js/visualize/HiPSUtil.js @@ -20,11 +20,16 @@ import {getFireflySessionId} from '../Firefly'; import {encodeServerUrl, getRootURL, loadImage} from '../util/WebUtil.js'; import {CoordinateSys} from './CoordSys.js'; import {CysConverter} from './CsysConverter.js'; -import {getFoV, primePlot} from './PlotViewUtil.js'; +import {dispatchPlotImage} from './ImagePlotDispatch'; +import {findViewerWithItemId, getMultiViewRoot} from './MultiViewCntlr'; +import {currentP, getFoV, primePlot} from './PlotViewUtil.js'; import {makeDevicePt, makeWorldPt} from './Point.js'; import {makeHiPSProjection} from './projection/Projection'; +import RangeValues, {ABSOLUTE_STR, PERCENTAGE_STR} from './RangeValues'; +import {IMAGE} from './VisConst'; import {computeDistance, convertCelestial, toDegrees, toRadians} from './VisUtil.js'; -import {changeHiPSProjectionCenter, getScreenPixScaleArcSec, isHiPSAitoff} from './WebPlot.js'; +import {changeHiPSProjectionCenter, getScreenPixScaleArcSec, isHiPS, isHiPSAitoff} from './WebPlot.js'; +import WebPlotRequest, {TitleOptions} from './WebPlotRequest'; export const MAX_SUPPORTED_HIPS_LEVEL= ORDER_MAX-2; @@ -97,7 +102,7 @@ export function getTilePixelAngSize(nOrder) { /** * - * @param {WebPlot} plot + * @param {WebPlot|undefined} plot * @param {boolean} [limitToImageDepth] When true, do not return a number that is greater than what this HiPS map * can display. Use hipsProperties.hips_order to determine. * @return {{useAllSky:boolean, norder:number, desiredNorder:number, isMaxOrder:boolean}} norder is the result, useAllSky true when the norder is 2 or 3 but @@ -184,14 +189,43 @@ function getCatalogNOrderForPixArcSecSize(sizeInArcSec) { return norder; } +/** + * @param plot + * @param nOrder + * @param tileNumber + * @return {string|null} + */ export function makeHiPSTileUrl(plot, nOrder, tileNumber) { if (!plot) return null; + return makeHipsUrl(makeHipsTilePath(plot,nOrder,tileNumber), + plot.proxyHips, plot.hipsFromHipsList); +} + +/** + * @param plot + * @param nOrder + * @param tileNumber + * @param [ext] + * @return {string|null} + */ +function makeHipsTilePath(plot,nOrder,tileNumber,ext) { const dir= Math.floor(tileNumber/10000)*10000; - const exts= plot.hipsProperties?.hips_tile_format ?? 'jpg'; + const exts= ext ?? getHiPSTileExt(plot.hipsProperties?.hips_tile_format ?? 'jpg'); const cubeExt= plot.cubeDepth>1 && plot.cubeIdx>0 ? '_'+plot.cubeIdx : ''; const root= plot.hipsUrlRoot.endsWith('/') ? plot.hipsUrlRoot : plot.hipsUrlRoot+'/'; - return makeHipsUrl(`${root}Norder${nOrder}/Dir${dir}/Npix${tileNumber}${cubeExt}.${getHiPSTileExt(exts)}`, - plot.proxyHips, plot.hipsFromHipsList); + return `${root}Norder${nOrder}/Dir${dir}/Npix${tileNumber}${cubeExt}.${exts}`; +} + +export function makeHipsFitsTilePath(plot,nOrder,tileNumber) { + let tileUrl; + if (plot.hasFitsCube && plot.cubeDepth>1) { + tileUrl= makeHipsTilePath(plot,nOrder,tileNumber,'fits'); + tileUrl= tileUrl.replace(/_\d*.fits/,'_cube.fits'); + } + else { + tileUrl= makeHipsTilePath(plot,nOrder,tileNumber,'fits'); + } + return tileUrl; } /** @@ -397,6 +431,25 @@ export function getHealpixPixel(plot, wp) { } +/** + * + * @param norder + * @param wp + * @param {CoordinateSys} dataCoordSys + * @return {Number} + * @return {{ipix:number, wpCorners:Array., norder:number}} contains the healpix pixel number and a worldPt array of corners + */ +export function getHealpixCellAtNorder(norder, wp, dataCoordSys) { + const dataWp= convertCelestial(wp, dataCoordSys); + const polar = radecToPolar(dataWp.x,dataWp.y); + const ipix= ang2pixNest(polar.theta, polar.phi,2**(norder)); + const nside= 2**norder; + return {...healpixCache.makeCornersForPix(ipix, nside, dataCoordSys),norder}; +} + + + + const twoPos= [256,128,64,32,16,8,4,2,1]; /** @@ -679,4 +732,56 @@ export async function loadImageMultiCall(url) { }); } - +export function extractFitsFromHiPS(plot,nOrder,tileNumber) { + if (!plot.hasFits) return; + + const {min,max}= getHipsDataRange(plot); + const rv= getHipsPixelCutRangeValues(plot); + + const {pv} = currentP(plot.plotId); + const tileUrl= makeHipsFitsTilePath(plot,nOrder,tileNumber); + const wpRequest= WebPlotRequest.makeURIPlotRequest(tileUrl); + const plotId= `hipsExtract-${nOrder}-${tileNumber}`; + wpRequest.setTitle(`Tile: ${nOrder} / ${tileNumber}`); + wpRequest.setTitleOptions(TitleOptions.NONE); + wpRequest.setPlotId(plotId); + wpRequest.setInitialRangeValues(rv); + wpRequest.setInitialColorTable('0'); + const pvOptions= {}; + pvOptions.userCanDeletePlots= true; + wpRequest.setPlotGroupId(pv.plotGroupId); + + const {viewerId}= findViewerWithItemId(getMultiViewRoot(),plot.plotId, IMAGE) ?? {}; + dispatchPlotImage({ plotId, wpRequest, viewerId, pvOptions, }); +} + +function getHipsDataRange(plot) { + if (!isHiPS(plot) || !plot.hipsProperties?.hips_data_range) return {min:0,max:0}; + const mmStrAry= plot.hipsProperties.hips_data_range.split(' '); + if (mmStrAry.length===2) { + const min= Number(mmStrAry[0]); + const max= Number(mmStrAry[1]); + if (!isNaN(min) && !isNaN(max) && min < max) { + return {min,max}; + } + } + return {min:0,max:0}; +} + +function getHipsPixelCutRangeValues(plot) { + if (!isHiPS(plot) || !plot.hipsProperties?.hips_pixel_cut) return RangeValues.makeSimple(PERCENTAGE_STR,1,99,'linear'); + + const stretchValues= plot.hipsProperties.hips_pixel_cut.split(' '); + if (stretchValues.length>=2) { + const min= Number(stretchValues[0]); + const max= Number(stretchValues[1]); + if (!isNaN(min) && !isNaN(max) && min < max) { + const algorythm= (stretchValues.length>2 && RangeValues.isAlgorithmSupported(stretchValues[2])) + ? stretchValues[2] : 'log'; + const rv= RangeValues.makeSimple(ABSOLUTE_STR,min,max,algorythm); + if (algorythm==='asinh') rv.asinhQValue= 1; + return rv; + } + } + return RangeValues.makeSimple(PERCENTAGE_STR,1,99,'linear'); +} diff --git a/src/firefly/js/visualize/ImagePlotCntlr.js b/src/firefly/js/visualize/ImagePlotCntlr.js index 51a5dce15b..43a8b774b1 100644 --- a/src/firefly/js/visualize/ImagePlotCntlr.js +++ b/src/firefly/js/visualize/ImagePlotCntlr.js @@ -97,7 +97,7 @@ const initState= () => { //-- wcs match parameters positionLock: false, // images are locked together - wcsMatchCenterWP: null, // the point to match to + wcsMatchCenterWP: undefined, // the point to match to wcsMatchType: false, // one of 'Standard', 'Target', 'Pixel', 'PixelCenter', or false mpwWcsPrimId: null, // the plotId others are match to }; @@ -177,6 +177,11 @@ export default { reducers, actionCreators}; //======================================== Action Creators ============================= //======================================== Action Creators ============================= +const changeHipsActionCreator= (rawAction) => + (dispatcher, getState) => { + + }; + /** * @param {Action} rawAction diff --git a/src/firefly/js/visualize/MenuItemKeys.js b/src/firefly/js/visualize/MenuItemKeys.js index ba08ed5019..bb7b6390e5 100644 --- a/src/firefly/js/visualize/MenuItemKeys.js +++ b/src/firefly/js/visualize/MenuItemKeys.js @@ -39,6 +39,7 @@ export function getDefMenuItemKeys() { extractZAxis: true, extractLine: true, extractPoint: true, + extractHiPSTile: true, extract: true, showImageToolbar: true, hipsSurveyPopup: true, diff --git a/src/firefly/js/visualize/MouseReadoutCntlr.js b/src/firefly/js/visualize/MouseReadoutCntlr.js index 6e867a1887..22f14f26a3 100644 --- a/src/firefly/js/visualize/MouseReadoutCntlr.js +++ b/src/firefly/js/visualize/MouseReadoutCntlr.js @@ -21,6 +21,7 @@ export const DESC_VAL= 'desc'; export const STATUS_UNAVAILABLE= 'UNAVAILABLE'; +export const STATUS_RETRIEVING= 'RETRIEVING'; export const STATUS_NAN= 'NaN'; export const STATUS_UNDEFINED= 'UNDEFINED'; export const STATUS_VALUE= 'VALUE'; diff --git a/src/firefly/js/visualize/PlotAttribute.js b/src/firefly/js/visualize/PlotAttribute.js index abd9738056..6bade20f0c 100644 --- a/src/firefly/js/visualize/PlotAttribute.js +++ b/src/firefly/js/visualize/PlotAttribute.js @@ -106,6 +106,11 @@ export const PlotAttribute= { */ ACTIVE_POINT: 'ACTIVE_POINT', + /** * The active hips tile last clicked on */ + ACTIVE_HIPS_CELL: 'ACTIVE_HIPS_CELL', + + /** * The plot norder of the hips tile clicked on */ + ACTIVE_HIPS_NORDER: 'ACTIVE_HIPS_NORDER', /** * if set, must be one of the string values defined by the enum ZoomUtil.FullType diff --git a/src/firefly/js/visualize/PlotViewUtil.js b/src/firefly/js/visualize/PlotViewUtil.js index 11866edc14..bf93c97603 100644 --- a/src/firefly/js/visualize/PlotViewUtil.js +++ b/src/firefly/js/visualize/PlotViewUtil.js @@ -813,14 +813,20 @@ export function getNumberOfCubesInPV(pv) { } /** - * Get the total number of planes in the cube of the plot + * Get the total number of planes in the cube of the plot either image or hips * @param {PlotView|WebPlot} plotOrPv * @return {number} the number of cube planes, 0 if this is not a cube */ -export const getCubePlaneCnt= (plotOrPv) => { +export const getCubeLength= (plotOrPv) => { const plot= isPlotView(plotOrPv) ? primePlot(plotOrPv) : plotOrPv; - if (!isImageCube(plot)) return 0; - return plot.cubeCtx.cubeLength; + if (!plot) return 0; + if (isImage(plot)) { + if (!isCube(plot)) return 0; + return plot.cubeCtx.cubeLength; + } + else { + return (plot.cubeDepth>1) ? plot.cubeDepth : 0; + } }; @@ -868,19 +874,27 @@ export function getHDUIndex(pv, plot= undefined) { export const hasImageCubes = (pv) => getNumberOfCubesInPV(pv)>0; /** - * get the plane index of this plot in cube + * get the plane index of this plot in a cube either hips or image * @param {WebPlot|undefined} plot * @return {number} the plane index, -1 if not in a cube */ -export const getImageCubeIdx = (plot) => plot?.cubeCtx?.cubePlane ?? -1; +export function getCubePlaneIdx(plot) { + if (!plot) return -1; + return isImage(plot) + ? (plot.cubeCtx?.cubePlane ?? -1) + : plot.cubeIdx; +} /** - * plot is plane in a image cube + * plot is plane in a cube either hips or image * @param {WebPlot|undefined} plot * @return {boolean} true if plot is a plane in a cube, otherwise false */ -export const isImageCube = (plot) => getImageCubeIdx(plot) > -1; +export function isCube(plot) { + if (!plot) return false; + return isImage(plot) ? Boolean(plot.cubeCtx) : plot.cubeDepth>1; +} /** * Given a HDU index and optionally a cube index, return the image idx @@ -893,29 +907,29 @@ export function convertHDUIdxToImageIdx(pv, hduIdx, cubeIdx=0) { if (!pv || !isPlotView(pv)) return undefined; if (!isMultiImageFits(pv)) return 0; const plot= primePlot(pv); - if (cubeIdx==='follow' && isImageCube(plot)) { + if (cubeIdx==='follow' && isCube(plot)) { const idx= pv.plots.findIndex((p)=> p===plot); cubeIdx= convertImageIdxToHDU(pv,idx).cubeIdx; } const startIndexes= getHduPlotStartIndexes(pv); if (hduIdx>startIndexes.length-1)return 0; - const cnt= getCubePlaneCnt(pv.plots[startIndexes[hduIdx]]); - return (isImageCube(pv.plots[startIndexes[hduIdx]]) && cubeIdxpv.plots.length-1) return {hduIdx:0, cubeIdx:undefined, isCube:false}; - const isCube= isImageCube(pv.plots[imageIdx]); + const isPlotCube= isCube(pv.plots[imageIdx]); const startIndexes= getHduPlotStartIndexes(pv); const hduIdx=getHDUIndex(pv,pv.plots[imageIdx]); - return {hduIdx, cubeIdx:isCube && imageIdx-startIndexes[hduIdx], isCube}; + return {hduIdx, cubeIdx:isPlotCube && imageIdx-startIndexes[hduIdx], cube:isPlotCube}; } @@ -955,8 +969,8 @@ export const hasPlaneOnlyWLInfo= (plot) => Boolean(plot?.wlData?.hasPlainOnlyCoo */ export function getAllWaveLengthsForCube(pv,imPt) { const plot= primePlot(pv); - if (!plot || !isImageCube(plot) || !hasWLInfo(plot)) return; - const len= getCubePlaneCnt(pv); + if (!plot || !isCube(plot) || !hasWLInfo(plot)) return; + const len= getCubeLength(pv); if (!len) return; return Array(len).fill('').map( (v,i) => getPtWavelength(plot, imPt, i)); } @@ -988,6 +1002,8 @@ const getWlUnitValue= (plot, idx, band= Band.NO_BAND) => hasWLInfo(plot) ? plot.wlDataAry[band.value]?.spectralCoords?.[idx]?.units : ''; +const micronRe= new RegExp('microns|micron|um|micrometers','gi'); + /** * * @param {WebPlot|String} plotOrStr - pass a WebPlot to get the units from and the format or a string that will be formatted @@ -998,7 +1014,7 @@ export function getFormattedWaveLengthUnits(plotOrStr, anyPartOfStr=false) { const MICRON_SYMBOL= WAVELENGTH_UNITS.um.symbol; const uStr= isString(plotOrStr) ? plotOrStr : getWaveLengthUnits(plotOrStr); if (anyPartOfStr) { - return uStr.replace(new RegExp('microns|micron|um|micrometers','gi'),MICRON_SYMBOL); + return uStr.replace(micronRe, MICRON_SYMBOL); } else { const u= uStr.toLowerCase(); @@ -1006,6 +1022,13 @@ export function getFormattedWaveLengthUnits(plotOrStr, anyPartOfStr=false) { } } +export function getWaveLengthUnitsPrecision(plotOrStr) { + if (!plotOrStr) return 4; + const uStr= isString(plotOrStr) ? plotOrStr : getWaveLengthUnits(plotOrStr); + if (!uStr) return 4; + if (uStr.match(micronRe)?.[0]) return 3; + return 4; +} /** * diff --git a/src/firefly/js/visualize/RangeValues.js b/src/firefly/js/visualize/RangeValues.js index 2386b567e6..591ae0bc57 100644 --- a/src/firefly/js/visualize/RangeValues.js +++ b/src/firefly/js/visualize/RangeValues.js @@ -28,6 +28,7 @@ export const STRETCH_POWERLAW_GAMMA = 51; const LINEAR_STR= 'linear'; const alStrToConst = { + linear: STRETCH_LINEAR, log : STRETCH_LOG, loglog : STRETCH_LOGLOG, equal : STRETCH_EQUAL, @@ -269,6 +270,10 @@ export class RangeValues { rv.asinhStretch+','+ rv.scalingK; } + + static isAlgorithmSupported(a) { + return Boolean(alStrToConst[a]); + } } diff --git a/src/firefly/js/visualize/VisMouseSync.js b/src/firefly/js/visualize/VisMouseSync.js index 893b5a381a..87386a51a2 100644 --- a/src/firefly/js/visualize/VisMouseSync.js +++ b/src/firefly/js/visualize/VisMouseSync.js @@ -127,6 +127,7 @@ export function makeMouseStatePayload(plotId,mouseState,screenPt,screenX,screenY if (result) { payload.healpixPixel= result.pixel; payload.norder= result.norder; + payload.tileCoords= result.tileCoords; } } return payload; diff --git a/src/firefly/js/visualize/WebPlot.js b/src/firefly/js/visualize/WebPlot.js index 44a9b33a69..d46a1f38a9 100644 --- a/src/firefly/js/visualize/WebPlot.js +++ b/src/firefly/js/visualize/WebPlot.js @@ -3,13 +3,12 @@ */ import {ZoomType} from './VisConst'; import {isArray, isBoolean, isEmpty, isNumber, isUndefined} from 'lodash'; -import {isDefined, memorizeLastCall} from '../util/WebUtil'; +import {isDefined, memorizeLastCall, splitByWhiteSpace} from '../util/WebUtil'; import {allBandAry, Band} from './Band'; import CoordinateSys from './CoordSys.js'; import {CysConverter} from './CsysConverter.js'; import PlotState, {makePlotStateShimForHiPS} from './PlotState'; -import {makeImagePt} from './Point'; -import {makeDevicePt, makeScreenPt, makeWorldPt} from './Point.js'; +import {makeImagePt, makeDevicePt, makeScreenPt, makeWorldPt} from './Point.js'; import { HIPS_AITOFF, HIPS_DATA_HEIGHT, HIPS_DATA_WIDTH, makeHiPSProjection, makeProjectionNew, UNRECOGNIZED, UNSPECIFIED } from './projection/Projection.js'; @@ -580,7 +579,7 @@ export const WebPlot= { * @param {boolean} proxyHips - if true use the proxy (firefly server) to get the hips tailes * @return {WebPlot} the new WebPlot object for HiPS */ - makeWebPlotDataHIPS(plotId, hipsUrlRoot, wpRequest, hipsProperties, attributes= {}, proxyHips) { + makeWebPlotDataHIPS(plotId, hipsUrlRoot, wpRequest, hipsProperties={}, attributes= {}, proxyHips) { const blank= isBlankHiPSURL(wpRequest.getHipsRootUrl()); const hipsCoordSys= makeHiPSCoordSys(hipsProperties); @@ -588,6 +587,12 @@ export const WebPlot= { const lat= blank ? 0 : Number(hipsProperties.hips_initial_dec) || 0; const projection= makeHiPSProjection(hipsCoordSys, lon,lat, false); const plot= makePlotTemplate(plotId,'hips',false, hipsCoordSys); + + const formatAry= splitByWhiteSpace(hipsProperties.hips_tile_format); + const hasFitsCube= splitByWhiteSpace(hipsProperties.custom_tile_format).includes('fits-cube'); + const hasFits= formatAry.includes('fits') || hasFitsCube; + const cubeDepth= Number(hipsProperties?.hips_cube_depth) || 1; + const zoomFactor= .0001; const hipsPlot= { @@ -597,6 +602,8 @@ export const WebPlot= { hipsUrlRoot, dataCoordSys : hipsCoordSys, hipsProperties, + hasFits, + hasFitsCube, proxyHips, /// other @@ -611,7 +618,7 @@ export const WebPlot= { dataDesc : hipsProperties.label || 'HiPS', blank, blankColor: 'rgba(55,55,55,1)', - cubeDepth: Number(hipsProperties?.hips_cube_depth) || 1, + cubeDepth, //=== Mutable ===================== screenSize: {width:HIPS_DATA_WIDTH*zoomFactor, height:HIPS_DATA_HEIGHT*zoomFactor}, cubeIdx: Number(hipsProperties?.hips_cube_firstframe) || 0, @@ -936,3 +943,5 @@ export function processHeaderData(wpInit, plotState) { return headerInfo; } + + diff --git a/src/firefly/js/visualize/WebPlotRequest.js b/src/firefly/js/visualize/WebPlotRequest.js index 8b170dc47b..db2d2d0a32 100644 --- a/src/firefly/js/visualize/WebPlotRequest.js +++ b/src/firefly/js/visualize/WebPlotRequest.js @@ -14,7 +14,8 @@ const DEFAULT_IMAGE_OVERLAYS= ['ACTIVE_TARGET_TYPE','POINT_SELECTION_TYPE', 'NOR ]; const DEFAULT_HIPS_OVERLAYS= ['ACTIVE_TARGET_TYPE','POINT_SELECTION_TYPE', 'NORTH_UP_COMPASS_TYPE', - 'OVERLAY_MARKER_TYPE', 'OVERLAY_FOOTPRINT_TYPE', 'REGION_PLOT_TYPE', 'HIPS_GRID_TYPE', 'MOC_PLOT_TYPE']; + 'OVERLAY_MARKER_TYPE', 'OVERLAY_FOOTPRINT_TYPE', 'REGION_PLOT_TYPE', + 'HIPS_GRID_TYPE', 'EXTRACT_HIPS_FILE_TOOL_TYPE', 'MOC_PLOT_TYPE']; /** * @typedef ServiceType diff --git a/src/firefly/js/visualize/reducer/HandlePlotChange.js b/src/firefly/js/visualize/reducer/HandlePlotChange.js index 230cb8d5dd..da07f913fc 100644 --- a/src/firefly/js/visualize/reducer/HandlePlotChange.js +++ b/src/firefly/js/visualize/reducer/HandlePlotChange.js @@ -32,7 +32,7 @@ import { primePlot, clonePvAry, clonePvAryWithPv, applyToOnePvOrAll, applyToOnePvOrOverlayGroup, matchPlotViewByPositionGroup, getPlotViewIdxById, getPlotGroupIdxById, findPlotGroup, getPlotViewById, findCurrentCenterPoint, getCenterOfProjection, - isRotationMatching, hasWCSProjection, isThreeColor, getHDU, getMatchingRotationAngle, isImageCube, + isRotationMatching, hasWCSProjection, isThreeColor, getHDU, getMatchingRotationAngle, isCube, convertImageIdxToHDU, hasLocalStretchByteData } from '../PlotViewUtil.js'; import Point, {parseAnyPt, makeImagePt, makeWorldPt, makeDevicePt} from '../Point.js'; @@ -718,7 +718,7 @@ function makeNewPrimePlot(state,action) { if (!existingPv || isEmpty(existingPv.plots) || existingPv.plots.length<=primeIdx) return state; const pv= changePrimePlot(existingPv, primeIdx); - if (isImageCube(primePlot(pv))) { + if (isCube(primePlot(pv))) { const primeIdx= convertImageIdxToHDU(pv,pv.primeIdx).cubeIdx; pv.overlayPlotViews= pv.overlayPlotViews.map( (oPv) => { if (!oPv.cube || !oPv.plot || !oPv.plots.length) return oPv; diff --git a/src/firefly/js/visualize/reducer/HandlePlotCreation.js b/src/firefly/js/visualize/reducer/HandlePlotCreation.js index fe89405f7f..02470861ff 100644 --- a/src/firefly/js/visualize/reducer/HandlePlotCreation.js +++ b/src/firefly/js/visualize/reducer/HandlePlotCreation.js @@ -15,7 +15,7 @@ import { import {makeOverlayPlotView, initOverlayPlots} from './OverlayPlotView.js'; import { primePlot, getPlotViewById, clonePvAry, getOverlayById, getPlotViewIdListByPositionLock, - getCubePlaneCnt, getHDU, getImageCubeIdx + getCubeLength, getHDU, getCubePlaneIdx } from '../PlotViewUtil.js'; import {getPlotGroupById, makePlotGroup} from '../PlotGroup.js'; import {PlotAttribute} from '../PlotAttribute.js'; @@ -166,8 +166,8 @@ function addHiPS(state,action, setActive= true, newPlot= true) { function countCubes(pv) { if (!pv || !isImage(primePlot(pv)) ) return 0; return pv.plots.reduce( (total, p, idx) => { - if (idx===0) return getImageCubeIdx(p)>=0 ? 1 : 0; - return ( getHDU(p)!==getHDU(pv.plots[idx-1]) && getImageCubeIdx(p)>-1) ? total+1 : total; + if (idx===0) return getCubePlaneIdx(p)>=0 ? 1 : 0; + return ( getHDU(p)!==getHDU(pv.plots[idx-1]) && getCubePlaneIdx(p)>-1) ? total+1 : total; }, 0); } @@ -190,12 +190,12 @@ function addPlot(state,action, setActive, newPlot) { pv.plotViewCtx.multiHdu= hduCnt.length>1; pv.plotViewCtx.cubeCnt= countCubes(pv); pv.plotViewCtx.hduPlotStartIndexes= pv.plotViewCtx.multiHdu ? - pv.plots.map( (p,idx) => idx).filter( (idx) => getImageCubeIdx(pv.plots[idx])<1) : [0]; + pv.plots.map( (p,idx) => idx).filter( (idx) => getCubePlaneIdx(pv.plots[idx])<1) : [0]; if (pv.plotViewCtx.cubeCnt>0) { const firstCubePlotIdx= pv.plots.findIndex( (p) => p.cubeIdx>-1); - const cnt= getCubePlaneCnt(pv.plots[firstCubePlotIdx]); + const cnt= getCubeLength(pv.plots[firstCubePlotIdx]); const frameIdx= getFirstFrameFromAttribute(pv,cnt); if (frameIdx>0) { diff --git a/src/firefly/js/visualize/saga/MouseReadoutWatch.js b/src/firefly/js/visualize/saga/MouseReadoutWatch.js index cc389f79a0..bb008be92b 100644 --- a/src/firefly/js/visualize/saga/MouseReadoutWatch.js +++ b/src/firefly/js/visualize/saga/MouseReadoutWatch.js @@ -11,15 +11,18 @@ import { } from '../MouseReadoutCntlr.js'; import {callGetFileFlux} from '../../rpc/PlotServicesJson.js'; import {allBandAry, Band} from '../Band.js'; +import {makeImagePt} from '../Point'; import {MouseState} from '../VisMouseSync.js'; import CsysConverter, {CysConverter} from '../CsysConverter.js'; import {getPixScale, getScreenPixScale, getScreenPixScaleArcSec, isImage, isHiPS, getFluxUnits} from '../WebPlot.js'; -import {getPlotTilePixelAngSize} from '../HiPSUtil.js'; +import { + getHealpixCellAtNorder, getHiPSNorderlevel, getPlotTilePixelAngSize, makeHipsFitsTilePath +} from '../HiPSUtil.js'; import {mouseUpdatePromise, fireMouseReadoutChange} from '../VisMouseSync'; import { - primePlot, getPlotStateAry, getImageCubeIdx, + primePlot, getPlotStateAry, getCubePlaneIdx, getWavelengthParseFailReason, getWaveLengthUnits, hasPixelLevelWLInfo, hasPlaneOnlyWLInfo, - isImageCube, wavelengthInfoParsedSuccessfully, getPtSpectralCoords, getBandWidthUnits, isThreeColor, currentP, + isCube, wavelengthInfoParsedSuccessfully, getPtSpectralCoords, getBandWidthUnits, isThreeColor, currentP, } from '../PlotViewUtil'; import {getFluxRadix} from 'firefly/visualize/ui/MouseReadoutUIUtil'; @@ -57,7 +60,7 @@ export function* watchReadout() { let getNextWithWithAsync= false; const lockByClick= isLockByClick(readoutRoot()); let {worldPt,screenPt,imagePt}= mouseCtx; - const {plotId,mouseState, healpixPixel, norder, shiftDown,eventType}= mouseCtx; + const {plotId,mouseState, healpixPixel, norder, shiftDown,eventType, tileCoords}= mouseCtx; const useEv= !igoreEvTypes.includes(eventType); if (!useEv) { @@ -79,7 +82,7 @@ export function* watchReadout() { if (isPayloadNeeded(mouseState,lockByClick)) { if (plot) { - const readoutItems= makeImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder); + const readoutItems= makeImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords); fireMouseReadoutChange({plotId, readoutItems, threeColor, readoutType:getReadoutKey(plot)}); getNextWithWithAsync= hasAsyncReadout(plot); } @@ -91,8 +94,8 @@ export function* watchReadout() { if (useEv && getNextWithWithAsync) { // get the next mouse event or the flux mouseCtx= lockByClick || eventType=== 'touchstart' ? - yield call(processAsyncDataImmediate,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) : - yield call(processAsyncDataDelayed,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder); + yield call(processAsyncDataImmediate,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords) : + yield call(processAsyncDataDelayed,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords); } else { // get the next mouse event mouseCtx = yield call(mouseUpdatePromise); @@ -101,9 +104,9 @@ export function* watchReadout() { } } -function* processAsyncDataImmediate(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) { +function* processAsyncDataImmediate(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords) { try { - const readoutItems= yield call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder); + const readoutItems= yield call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords); if (readoutItems) { const plot= primePlot(plotView); // dispatchReadoutData({plotId:plotView.plotId,readoutItems, threeColor, readoutKey:getReadoutKey(plot)}); @@ -120,7 +123,7 @@ function* processAsyncDataImmediate(plotView, worldPt, screenPt, imagePt, threeC } -function* processAsyncDataDelayed(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) { +function* processAsyncDataDelayed(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords) { const plot= primePlot(plotView); const mousePausedRaceWinner = yield race({ mouseCtx: call(mouseUpdatePromise), timer: call(delay, PAUSE_DELAY) }); @@ -129,7 +132,7 @@ function* processAsyncDataDelayed(plotView, worldPt, screenPt, imagePt, threeCol try { const mouseMoveRaceWinner = yield race({ mouseCtx: call(mouseUpdatePromise), - readoutItems: call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) + readoutItems: call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder, tileCoords) }); if (mouseMoveRaceWinner.mouseCtx) return mouseMoveRaceWinner.mouseCtx; @@ -170,9 +173,9 @@ function makeImmediateReadout(plot,worldPt,screenPt,imagePt, threeColor, healpix return rt && rt.createImmediateReadout(plot,worldPt,screenPt,imagePt, threeColor, healpixPixel, norder); } -function makeAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor) { +function makeAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor, healpixPixel, norder, tileCoords) { const rt= getReadoutType(primePlot(plotView)); - return Promise.resolve(rt && rt.createAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor)); + return Promise.resolve(rt && rt.createAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor, healpixPixel, norder, tileCoords)); } function hasAsyncReadout(plot) { @@ -236,8 +239,8 @@ const readoutTypes= [ readoutKey: HIPS_STANDARD_READOUT, matches: (plot) => isHiPS(plot), createImmediateReadout: makeHiPSReadout, - createAsyncReadout: () => {throw Error('HiPS should not do async');}, - hasAsyncReadout: (plot) => false, + createAsyncReadout: makeHiPSPlotAsyncReadout, + hasAsyncReadout: (plot) => plot.hasFits, }, ]; @@ -262,8 +265,25 @@ function makeImagePlotAsyncReadout(plotView, worldPt, screenPt, imagePt, threeCo return doFluxCall(plotView,imagePt).then( (fluxResult) => { return makeReadoutWithFlux(readoutItems,primePlot(plotView), fluxResult, radix, threeColor); }); + +} + +function makeHiPSPlotAsyncReadout(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, inNorder, tileCoords) { + const plot = primePlot(plotView); + const readoutItems = makeImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, inNorder); + const radix = 10; + + const {norder}= getHiPSNorderlevel(plot,true); + const cell = getHealpixCellAtNorder(norder, worldPt, plot.dataCoordSys); + if (!cell || norder<1) return; + const hipsTileUrl= makeHipsFitsTilePath(plot,norder,cell.ipix); + const tileImagePt= makeImagePt(tileCoords.x,512-tileCoords.y-1); + return doFluxCall(plotView, tileImagePt, worldPt, hipsTileUrl, plot.cubeIdx).then((fluxResult) => { + return makeReadoutWithFlux(readoutItems, primePlot(plotView), fluxResult, radix, false); + }); } + /** * * @param readout @@ -298,13 +318,22 @@ function makeReadoutWithFlux(readout, plot, fluxResult, radix, threeColor) { } -function doFluxCall(plotView,iPt) { +/** + * + * @param plotView + * @param iPt + * @param [wpt] + * @param [hipsTileUrl] + * @param [hipsPlane] + * @return {Promise|Promise|Promise>} + */ +function doFluxCall(plotView,iPt, wpt, hipsTileUrl=undefined, hipsPlane=0) { const plot= primePlot(plotView); if (CysConverter.make(plot).pointInPlot(iPt)) { const plotStateAry= getPlotStateAry(plotView); const passAry=[plotStateAry[0]]; if (plotStateAry[1]) passAry.push(plotStateAry[1]); - return callGetFileFlux(passAry, iPt) + return callGetFileFlux(passAry, iPt, wpt,isHiPS(plot), hipsTileUrl, hipsPlane) .then((result) => { return result; }) @@ -321,7 +350,8 @@ function doFluxCall(plotView,iPt) { function getFlux(result, plot) { const fluxArray = []; if (result.NO_BAND) { - fluxArray[0]= {...result.NO_BAND, unit: getFluxUnits(plot)}; + const unit= isHiPS(plot) ? result.NO_BAND.unit : getFluxUnits(plot); + fluxArray[0]= {...result.NO_BAND, unit}; } else { const bands = plot.plotState.getBands(); @@ -402,10 +432,10 @@ function showSingleBandFluxLabel(plot, band) { * @return {Object} */ function makeWLResult(plot,imagePt= undefined) { - if ((hasPixelLevelWLInfo(plot) || (hasPlaneOnlyWLInfo(plot) && !isImageCube(plot)))) { + if ((hasPixelLevelWLInfo(plot) || (hasPlaneOnlyWLInfo(plot) && !isCube(plot)))) { if (wavelengthInfoParsedSuccessfully(plot)) { if (!imagePt) return {}; - const cubeIdx= (isImageCube(plot) && getImageCubeIdx(plot)) || 0; + const cubeIdx= (isCube(plot) && getCubePlaneIdx(plot)) || 0; const specCoords= getPtSpectralCoords(plot, imagePt, cubeIdx); const result= { wl: makeValueReadoutItem('Wavelength', specCoords[0] ?? 0, getWaveLengthUnits(plot), 4), diff --git a/src/firefly/js/visualize/task/PlotHipsTask.js b/src/firefly/js/visualize/task/PlotHipsTask.js index 7b2c1a7422..eda634054e 100644 --- a/src/firefly/js/visualize/task/PlotHipsTask.js +++ b/src/firefly/js/visualize/task/PlotHipsTask.js @@ -20,7 +20,7 @@ import {dlRoot, getDlAry, visRoot} from '../VisStoreRoots'; import {makeWorldPt} from '../Point.js'; import {WebPlot, isHiPS, isImage, isBlankHiPSURL} from '../WebPlot.js'; import {PlotAttribute} from '../PlotAttribute.js'; -import {getStatusFromFetchError} from '../../util/WebUtil.js'; +import {getStatusFromFetchError, isDefined} from '../../util/WebUtil.js'; import { findCurrentCenterPoint, getCenterOfProjection, getCorners, getDrawLayerByType, getDrawLayersByType, getFoV, getPlotViewById, primePlot, @@ -38,7 +38,7 @@ import {addNewMocLayer, isMOCFitsFromUploadAnalsysis, makeMocTableId, MOCInfo, U import HiPSMOC from '../../drawingLayers/HiPSMOC.js'; import {getRowCenterWorldPt} from '../saga/ActiveRowToImageWatcher'; import {getActiveTableId, getTblById} from '../../tables/TableUtil'; -import {locateOtherIfMatched, matchHiPStoPlotView} from './WcsMatchTask'; +import {locateOtherIfMatched, matchCubePlanes, matchHiPStoPlotView} from './WcsMatchTask'; import {upload} from '../../rpc/CoreServices.js'; import {fetchUrl} from '../../util/fetch'; import {getGpuJs} from '../rawData/GpuJsConfig.js'; @@ -246,7 +246,7 @@ async function makeHiPSPlot(rawAction, dispatcher) { return; } await getGpuJs(); // make sure the GPU code is loaded up front - createHiPSGridLayer(); + createHiPSGridLayers(); dispatchAddActionWatcher({ actions:[PLOT_HIPS, UPDATE_VIEW_SIZE], callback:watchForHiPSViewDim, @@ -310,17 +310,14 @@ export async function createHiPSMocLayer({ivoid, title, hipsUrl, plot, visible=f } } catch (e) { - showInfoPopup('Could not find the MOC for: '+ title, - 'Moc Not Found'); // eslint-disable-line quotes + showInfoPopup('Could not find the MOC for: '+ title, 'Moc Not Found'); console.log(`MOC not found at URL (this is not uncommon): ${e}`) ; } } -function createHiPSGridLayer() { - const dl= getDrawLayerByType(getDlAry(), HiPSGrid.TYPE_ID); - if (!dl) { - dispatchCreateDrawLayer(HiPSGrid.TYPE_ID); - } +function createHiPSGridLayers() { + const gridDl= getDrawLayerByType(getDlAry(), HiPSGrid.TYPE_ID); + if (!gridDl) dispatchCreateDrawLayer(HiPSGrid.TYPE_ID); } @@ -342,6 +339,7 @@ async function doHiPSChange(rawAction, dispatcher, getState) { const newPayload= {...payload, blank}; dispatcher( { type: CHANGE_HIPS, payload:newPayload }); locateOtherIfMatched(visRoot(),plotId); + if (isDefined(payload.cubeIdx)) matchCubePlanes(plotId); dispatcher( { type: ANY_REPLOT, payload:newPayload }); return; } @@ -383,6 +381,7 @@ async function doHiPSChange(rawAction, dispatcher, getState) { }); initCorrectCoordinateSys(getPlotViewById(visRoot(), plotId)); locateOtherIfMatched(visRoot(),plotId); + if (isDefined(payload.cubeIdx)) matchCubePlanes(plotId); dispatcher({type: ANY_REPLOT, payload}); } catch (error) { console.log(error); diff --git a/src/firefly/js/visualize/task/WcsMatchTask.js b/src/firefly/js/visualize/task/WcsMatchTask.js index dcf8305951..692cd80403 100644 --- a/src/firefly/js/visualize/task/WcsMatchTask.js +++ b/src/firefly/js/visualize/task/WcsMatchTask.js @@ -5,7 +5,8 @@ import {isEmpty} from 'lodash'; import {dispatchAttachLayerToPlot, dispatchCreateDrawLayer} from '../DrawLayerDispatch'; import { - dispatchAttributeChange, dispatchChangeCenterOfProjection, dispatchChangeHiPS, dispatchFlip, dispatchPositionLocking, + dispatchAttributeChange, dispatchChangeCenterOfProjection, dispatchChangeHiPS, dispatchChangePrimePlot, + dispatchFlip, dispatchPositionLocking, dispatchRecenter, dispatchRotate, dispatchUpdateViewSize, dispatchZoom } from '../ImagePlotDispatch'; import { @@ -14,9 +15,11 @@ import { import {dlRoot, visRoot} from '../VisStoreRoots'; import {isEastLeftOfNorth, isPlotRotatedNorth} from '../WebPlotAnalysis'; import { - applyToOnePvOrAll, findCurrentCenterPoint, getCenterOfProjection, getCorners, getDrawLayerByType, + applyToOnePvOrAll, findCurrentCenterPoint, getCubePlaneIdx, getCenterOfProjection, getCorners, getCubeLength, + getDrawLayerByType, getMatchingRotationAngle, - getPlotViewAry, getPlotViewById, hasWCSProjection, isRotationMatching, primePlot, refreshP + getPlotViewAry, getPlotViewById, hasWCSProjection, isCube, isRotationMatching, operateOnOthersInPositionGroup, + primePlot, refreshP, currentP } from '../PlotViewUtil.js'; import {isHiPS, isImage} from '../WebPlot.js'; import {PlotAttribute} from '../PlotAttribute'; @@ -165,6 +168,7 @@ export function wcsMatchActionCreator(action) { payload: {wcsMatchCenterWP,wcsMatchType:false,mpwWcsPrimId:masterPv.plotId} }); } + if (isCube(masterPlot)) matchCubePlanes(masterPlot.plotId); }; } @@ -183,6 +187,30 @@ export function locateOtherIfMatched(vr,plotId) { } } +export function matchCubePlanes(plotId) { + setTimeout( + () => { + const {pv,plot}= currentP(plotId); + if (isCube(plot) && visRoot().positionLock) { + const idx= getCubePlaneIdx(plot); + const cubeLength= getCubeLength(pv); + operateOnOthersInPositionGroup(visRoot(), pv, + (testPv) => { + testPv= refreshP(testPv) ; + const testPlot= primePlot(testPv); + if (!isCube(testPlot)) return; + const {plotId}= testPlot; + if (getCubeLength(testPlot) === cubeLength && getCubePlaneIdx(testPlot) !== idx) { + isHiPS(testPlot) + ? dispatchChangeHiPS({plotId, cubeIdx:idx}) + : dispatchChangePrimePlot({plotId,primeIdx:idx}); + } + }, + true, true); + } + } ,5); +} + export const {matchImageToHips, matchHiPStoPlotView}= (() => { diff --git a/src/firefly/js/visualize/ui/Buttons.jsx b/src/firefly/js/visualize/ui/Buttons.jsx index a98c382b6e..7ad3430cdd 100644 --- a/src/firefly/js/visualize/ui/Buttons.jsx +++ b/src/firefly/js/visualize/ui/Buttons.jsx @@ -66,6 +66,7 @@ import OneXIcon from '@mui/icons-material/TimesOneMobiledataOutlined'; import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; import ChangeCircleIcon from '@mui/icons-material/ChangeCircle'; import ReverseIcon from '@mui/icons-material/FlipCameraAndroidOutlined'; +import ExtrqctTileIcon from '@mui/icons-material/FlipToBack'; import FiberManualRecordRoundedIcon from '@mui/icons-material/FiberManualRecordRounded'; @@ -169,6 +170,8 @@ export const ExtractLine= (props) => ( , sx:{'& .extractLine': {transform: 'rotate(142deg)'}}, ...props, }}/>); +export const ExtractTile= (props) => (, ...props, }}/>); + export const SearchDetailButton= (props) => ( , iconButtonSize:'44px', useDropDownIndicator: true, dropPosition: {left: 2, bottom: 0}, ...props diff --git a/src/firefly/js/visualize/ui/ColorDialog.jsx b/src/firefly/js/visualize/ui/ColorDialog.jsx index 11173a867e..b3db2119b6 100644 --- a/src/firefly/js/visualize/ui/ColorDialog.jsx +++ b/src/firefly/js/visualize/ui/ColorDialog.jsx @@ -66,7 +66,7 @@ function getStoreUpdate(oldS) { export const ColorDialog= memo(() => { const {plot,fields,rFields,gFields,bFields,rgbFields} = useStoreConnector(getStoreUpdate); - const [huePreserving, setHuePreserving]= useState(plot?.plotState.getRangeValues().rgbPreserveHue); + const [huePreserving, setHuePreserving]= useState(plot?.plotState?.getRangeValues()?.rgbPreserveHue ?? 0); if (!plot) return false; if (isImage(plot)) { diff --git a/src/firefly/js/visualize/ui/ExtractionWatchers.js b/src/firefly/js/visualize/ui/ExtractionWatchers.js index b9f3159d95..4a7b8d1a3a 100644 --- a/src/firefly/js/visualize/ui/ExtractionWatchers.js +++ b/src/firefly/js/visualize/ui/ExtractionWatchers.js @@ -15,7 +15,7 @@ import { import {getCellValue, getColumnIdx, getMetaEntry, getTblById} from 'firefly/tables/TableUtil.js'; import {MetaConst} from 'firefly/data/MetaConst.js'; import { - convertHDUIdxToImageIdx, getDrawLayerById, getHDU, getHDUIndex, getImageCubeIdx, isImageCube, primePlot + convertHDUIdxToImageIdx, getDrawLayerById, getHDU, getHDUIndex, getCubePlaneIdx, isCube, primePlot } from 'firefly/visualize/PlotViewUtil.js'; import {makeImagePt, parseImagePt} from 'firefly/visualize/Point.js'; import SearchTarget from 'firefly/drawingLayers/SearchTarget.js'; @@ -48,7 +48,7 @@ function zAxisExtractionPlotWatcher(action,cancelSelf,{tbl_id}) { } if (!isTargetCube(tbl_id)) return {tbl_id}; const {table,plot}= getInfo(tbl_id); - const cubeIdx= getImageCubeIdx(plot); + const cubeIdx= getCubePlaneIdx(plot); if (cubeIdx!==table.highlightedRow) dispatchTableHighlight(tbl_id,cubeIdx,table.request); return {tbl_id}; } @@ -70,7 +70,7 @@ function zAxisExtractionTableWatcher(action,cancelSelf,{tbl_id, drawLayerId=unde if ((type===TABLE_UPDATE || type===TABLE_LOADED) && !firstLoadComplete) { firstLoadComplete= true; const {table,plot}= getInfo(tbl_id); - const cubeIdx= getImageCubeIdx(plot); + const cubeIdx= getCubePlaneIdx(plot); dispatchTableHighlight(tbl_id,cubeIdx,table.request); return retData(); } @@ -119,7 +119,7 @@ function zAxisExtractionTableWatcher(action,cancelSelf,{tbl_id, drawLayerId=unde function isTargetCube(tbl_id) { const {table,pv,plot,extractionType,hdus,hduNum}= getInfo(tbl_id); if (!pv || !plot|| !table || extractionType!=='z-axis') return; - if (!isImageCube(plot)) return false; + if (!isCube(plot)) return false; const imPt= parseImagePt(getMetaEntry(table,MetaConst.FITS_IM_PT)); if (isEmpty(hdus) || !imPt || !Object.keys(hdus).includes(hduNum+'')) return false; if (getColumnIdx(table, 'plane') <0) return false; diff --git a/src/firefly/js/visualize/ui/FitsHeaderView.jsx b/src/firefly/js/visualize/ui/FitsHeaderView.jsx index b2e8f32fa2..9e17beef61 100644 --- a/src/firefly/js/visualize/ui/FitsHeaderView.jsx +++ b/src/firefly/js/visualize/ui/FitsHeaderView.jsx @@ -6,7 +6,7 @@ import {Stack, Typography} from '@mui/joy'; import React from 'react'; import DialogRootContainer from '../../ui/DialogRootContainer.jsx'; import {LayoutType, PopupPanel} from '../../ui/PopupPanel.jsx'; -import {primePlot, isImageCube, getCubePlaneCnt, currentP} from '../PlotViewUtil.js'; +import {primePlot, isCube, getCubeLength, currentP} from '../PlotViewUtil.js'; import {Tabs, Tab} from '../../ui/panel/TabPanel.jsx'; import {TablePanel} from '../../tables/ui/TablePanel.jsx'; import {dispatchShowDialog, dispatchHideDialog, isDialogVisible} from '../../core/ComponentCntlr.js'; @@ -215,7 +215,7 @@ function renderFileSizeAndPixelSize(plot, band, fitsHeaderInfo, isOnTab) { const fileSizeStr = getSizeAsString(tableModel?.tableMeta?.fileSize) ?? ''; let dimStr= `${plot.dataWidth} x ${plot.dataHeight}`; - if (isImageCube(plot)) dimStr+= ` x ${getCubePlaneCnt(plot)}`; + if (isCube(plot)) dimStr+= ` x ${getCubeLength(plot)}`; const overview= ( diff --git a/src/firefly/js/visualize/ui/MouseReadoutBottomLine.jsx b/src/firefly/js/visualize/ui/MouseReadoutBottomLine.jsx index 3c89436c13..39fd733644 100644 --- a/src/firefly/js/visualize/ui/MouseReadoutBottomLine.jsx +++ b/src/firefly/js/visualize/ui/MouseReadoutBottomLine.jsx @@ -8,6 +8,8 @@ import {object, bool, number} from 'prop-types'; import BrowserInfo from '../../util/BrowserInfo.js'; import {EMPTY_BUNIT_DEFAULT} from '../FitsHeaderUtil'; import {dispatchChangePointSelection} from '../ImagePlotDispatch'; +import {currentP} from '../PlotViewUtil'; +import {isHiPS} from '../WebPlot'; import {showMouseReadoutFluxRadixDialog} from './MouseReadoutOptionPopups.jsx'; import {getNonFluxDisplayElements, getFluxInfo} from './MouseReadoutUIUtil.js'; import {CopyToClipboard} from './MouseReadout.jsx'; @@ -20,6 +22,7 @@ export function MouseReadoutBottomLine({readout, readoutData, readoutShowing, st const {current:divref}= useRef({element:undefined}); const [haveDivRef,setHaveDivRef]= useState(false); + const {plot}= currentP(readoutData.plotId); useEffect( () => { setHaveDivRef(Boolean(divref.element)); @@ -30,8 +33,8 @@ export function MouseReadoutBottomLine({readout, readoutData, readoutShowing, st const {readoutType}= readoutData; if (!readoutData.readoutItems) return (
); - const isHiPS= readoutType===HIPS_STANDARD_READOUT; - const displayEle= getNonFluxDisplayElements(readoutData, readout.readoutPref, isHiPS); + const hips= isHiPS(plot); + const displayEle= getNonFluxDisplayElements(readoutData, readout.readoutPref, hips); const {readout1, showReadout1PrefChange, waveLength, bandWidth}= displayEle; const r1Value= readout1?.value ??''; const wlValue= waveLength?.value ??''; @@ -63,10 +66,10 @@ export function MouseReadoutBottomLine({readout, readoutData, readoutShowing, st const {threeColor= false}= readoutData; const monoFont= radix===16; - const doWL= ((r1Value && fullSize) || !r1Value) && waveLength && !isHiPS; - const doFlux= fullSize && !isHiPS; + const doWL= ((r1Value && fullSize) || !r1Value) && waveLength && !hips; + const doFlux= fullSize && (!hips || (hips && plot.hasFits)); - const lockByClickLabelWidth= isHiPS ? 450 : threeColor ? 750 : 600; + const lockByClickLabelWidth= hips ? 450 : threeColor ? 750 : 600; const checkboxText= width>lockByClickLabelWidth ? lockByClick ? 'Click Lock: on': 'Click Lock: off' : ''; const label3C= threeColor && doFlux ? get3CLabel(fluxArray) : ''; diff --git a/src/firefly/js/visualize/ui/MouseReadoutUIUtil.js b/src/firefly/js/visualize/ui/MouseReadoutUIUtil.js index 4e05c6785b..cda0ff48ab 100644 --- a/src/firefly/js/visualize/ui/MouseReadoutUIUtil.js +++ b/src/firefly/js/visualize/ui/MouseReadoutUIUtil.js @@ -11,7 +11,7 @@ import { MR_GALACTIC, MR_HEALPIX_NORDER, MR_HEALPIX_PIXEL, MR_PIXEL_SIZE, MR_SPIXEL_SIZE, MR_SUPER_GALACTIC, MR_WCS_COORDS, STATUS_NAN, STATUS_UNAVAILABLE, STATUS_UNDEFINED, STATUS_VALUE, TYPE_DECIMAL_INT, TYPE_EMPTY, TYPE_FLOAT, MR_BAND_WIDTH, EQ_TYPE, MR_WL_RED, MR_WL_GREEN, MR_WL_BLUE, MR_BAND_WIDTH_GREEN, MR_BAND_WIDTH_RED, - MR_BAND_WIDTH_BLUE + MR_BAND_WIDTH_BLUE, STATUS_RETRIEVING } from '../MouseReadoutCntlr.js'; import {convertCelestial} from '../VisUtil'; import {isCelestialImage} from '../WebPlot.js'; @@ -273,6 +273,7 @@ function makeFluxEntry(obj,radix=10) { const is16= radix===16; switch (status) { case STATUS_UNAVAILABLE: return {value: 'unavailable', label, unit:''}; + case STATUS_RETRIEVING: return {value: 'retrieving value', label, unit:''}; case STATUS_NAN: return is16 ? {value: `${valueBase16} (NaN)`, label, unit:''} : {value: 'NaN', label, unit:''}; case STATUS_UNDEFINED: return is16 ? {value: `${valueBase16} (undefined)`, label, unit:''} : {value: 'undefined', label, unit:''}; case STATUS_VALUE: diff --git a/src/firefly/js/visualize/ui/VisCtxToolbarView.jsx b/src/firefly/js/visualize/ui/VisCtxToolbarView.jsx index e213978caf..043b01f405 100644 --- a/src/firefly/js/visualize/ui/VisCtxToolbarView.jsx +++ b/src/firefly/js/visualize/ui/VisCtxToolbarView.jsx @@ -21,6 +21,7 @@ import {StateInputField} from '../../ui/StatedInputfield.jsx'; import {DropDownVerticalSeparator, ToolbarButton, ToolbarHorizontalSeparator} from '../../ui/ToolbarButton.jsx'; import BrowserInfo from '../../util/BrowserInfo.js'; import Validate from '../../util/Validate.js'; +import {splitByWhiteSpace} from '../../util/WebUtil'; import {CoordinateSys} from '../CoordSys.js'; import {getExtName, getExtType, getHeader} from '../FitsHeaderUtil.js'; import { @@ -28,9 +29,10 @@ import { } from '../ImagePlotDispatch'; import {PlotAttribute} from '../PlotAttribute'; import { - canConvertBetweenHipsAndFits, convertHDUIdxToImageIdx, convertImageIdxToHDU, currentP, getCubePlaneCnt, + canConvertBetweenHipsAndFits, convertHDUIdxToImageIdx, convertImageIdxToHDU, currentP, getCubeLength, getFormattedWaveLengthUnits, getHDU, getHDUCount, getHDUIndex, getPtWavelength, - hasPlaneOnlyWLInfo, isImageCube, isMultiHDUFits, primePlot, pvEqualExScroll, refreshP, + getWaveLengthUnitsPrecision, + hasPlaneOnlyWLInfo, isCube, isMultiHDUFits, primePlot, pvEqualExScroll, refreshP, } from '../PlotViewUtil.js'; import {makeWorldPt} from '../Point.js'; import {convertToHiPS, convertToImage, doHiPSImageConversionIfNecessary} from '../task/PlotHipsTask.js'; @@ -393,7 +395,7 @@ export function MultiImageControllerView({plotView:pv}) { let length; let wlStr= ''; let startStr; - const cube= isImageCube(plot) || !image; + const cube= isCube(plot) || !image; const multiHdu= isMultiHDUFits(pv); let hduDesc= ''; let tooltip; @@ -419,10 +421,11 @@ export function MultiImageControllerView({plotView:pv}) { tooltip+= `${reqHduInfo}HDU: ${hduNum} ${nameOrType?', '+hduDesc:''}`; } if (plot.cubeIdx>-1) { - tooltip+= `${multiHdu ? ', ':''} Cube: ${plot.cubeIdx+1}/${getCubePlaneCnt(plot)}`; + tooltip+= `${multiHdu ? ', ':''} Cube: ${plot.cubeIdx+1}/${getCubeLength(plot)}`; if (hasPlaneOnlyWLInfo(plot)) { - const wl= doFormat(getPtWavelength(plot,undefined, plot.cubeIdx),4); + const precision= getWaveLengthUnitsPrecision(plot); + const wl= doFormat(getPtWavelength(plot,undefined, plot.cubeIdx),precision); const unitStr= getFormattedWaveLengthUnits(plot); wlStr= wl ? `${wl} ${unitStr}` : ''; } @@ -475,20 +478,26 @@ const doFormat= (v,precision) => isNaN(v) function getHipsCubeDesc(plot) { if (!isHiPS(plot)) return ''; const {hipsProperties}= plot; - const {data_cube_crpix3, data_cube_crval3, data_cube_cdelt3, data_cube_bunit3=''}= hipsProperties; - if (!data_cube_crpix3 || !data_cube_crval3 || !data_cube_cdelt3) return ''; - const crpix3= Number(data_cube_crpix3); - const crval3= Number(data_cube_crval3); - const cdelt3= Number(data_cube_cdelt3); - const dp= Math.abs(cdelt3)>10 ? 0 : 1- Math.trunc(Math.log10(Math.abs(cdelt3))); // number of decimal points suggestion of gpdf - if (isNaN(crpix3) || isNaN(crval3) || isNaN(cdelt3)) return ''; - const value = crval3 + ( plot.cubeIdx - crpix3 ) * cdelt3; - const bunit3= (data_cube_bunit3!=='null' && data_cube_bunit3!=='nil' && data_cube_bunit3!=='undefined') ? - data_cube_bunit3 : ''; - return `${doFormat(value,dp)} ${getFormattedWaveLengthUnits(bunit3)}`; -} + const {data_cube_crpix3, data_cube_crval3, data_cube_cdelt3, data_cube_crval3_arr, data_cube_bunit3=''}= hipsProperties; + const cubePlanePropValue= data_cube_crval3_arr && splitByWhiteSpace(data_cube_crval3_arr)?.[plot.cubeIdx]; + const bunit3= (data_cube_bunit3 && data_cube_bunit3!=='null' && data_cube_bunit3!=='nil' && data_cube_bunit3!=='undefined') ? + data_cube_bunit3 : ''; + if (cubePlanePropValue) { + return `${cubePlanePropValue} ${getFormattedWaveLengthUnits(bunit3)}`; + } + else { + if (!data_cube_crpix3 || !data_cube_crval3 || !data_cube_cdelt3 || !cubePlanePropValue) return ''; + const crpix3= Number(data_cube_crpix3); + const crval3= Number(data_cube_crval3); + const cdelt3= Number(data_cube_cdelt3); + const dp= Math.abs(cdelt3)>10 ? 0 : 1- Math.trunc(Math.log10(Math.abs(cdelt3))); // number of decimal points suggestion of gpdf + if (isNaN(crpix3) || isNaN(crval3) || isNaN(cdelt3)) return ''; + const value = crval3 + ( plot.cubeIdx - crpix3 ) * cdelt3; + return `${doFormat(value,dp)} ${getFormattedWaveLengthUnits(bunit3)}`; + } +} function getEmLength(len) { const size= Math.trunc(Math.log10(len)) + 1; @@ -505,7 +514,7 @@ function FrameNavigator({pv, currPlotIdx, minForInput, displayType}) { getPlotIdx: (pv,idx) => convertHDUIdxToImageIdx(pv,idx, 'follow'), }, cube: { - getLen: getCubePlaneCnt, + getLen: getCubeLength, getContextIdx: (pv,idx) => convertImageIdxToHDU(pv,idx).cubeIdx, getPlotIdx: (pv,idx) => convertHDUIdxToImageIdx(pv, getHDUIndex(pv, primePlot(pv)), idx) }, diff --git a/src/firefly/js/visualize/ui/VisMiniToolbar.jsx b/src/firefly/js/visualize/ui/VisMiniToolbar.jsx index f9c54acbe4..ce38748e38 100644 --- a/src/firefly/js/visualize/ui/VisMiniToolbar.jsx +++ b/src/firefly/js/visualize/ui/VisMiniToolbar.jsx @@ -22,16 +22,18 @@ import {showRegionFileUploadPanel} from 'firefly/visualize/region/RegionFileUplo import {findUnactivatedRelatedData} from 'firefly/visualize/RelatedDataUtil.js'; import {ColorTableDropDownView, showColorDialog} from 'firefly/visualize/ui/ColorTableDropDownView.jsx'; import {showDrawingLayerPopup} from 'firefly/visualize/ui/DrawLayerPanel.jsx'; -import {endExtraction, LINE, POINTS, showExtractionDialog, Z_AXIS} from './extraction/ExtractionDialog.jsx'; +import {showExtractionDialog} from './extraction/ExtractionDialog.jsx'; import {showImageSelPanel} from 'firefly/visualize/ui/ImageSearchPanelV2.jsx'; import {MarkerDropDownView} from 'firefly/visualize/ui/MarkerDropDownView.jsx'; import {showMaskDialog} from 'firefly/visualize/ui/MaskAddPanel.jsx'; import {MatchLockDropDown} from 'firefly/visualize/ui/MatchLockDropDown.jsx'; import {showPlotInfoPopup} from 'firefly/visualize/ui/PlotInfoPopup.js'; +import {endExtraction, HIPS_TILE, LINE, POINTS, Z_AXIS} from './extraction/ExtractionUIUtil'; +import {showHiPSTileExtractionDialog} from './extraction/HiPSTileExtractionDialog'; import {SelectAreaButton} from './SelectAreaUIComponents.jsx'; import {SimpleLayerOnOffButton} from 'firefly/visualize/ui/SimpleLayerOnOffButton.jsx'; import {StretchDropDownView} from 'firefly/visualize/ui/StretchDropDownView.jsx'; -import {isHiPS} from 'firefly/visualize/WebPlot.js'; +import {isHiPS, isImage} from 'firefly/visualize/WebPlot.js'; import {getPreference} from '../../core/AppDataCntlr.js'; import {useStoreConnector} from '../../ui/SimpleComponent.jsx'; import { @@ -43,10 +45,11 @@ import {getDlAry, visRoot} from '../VisStoreRoots'; import {getMultiViewRoot, getViewer} from '../MultiViewCntlr.js'; import { getActivePlotView, getAllDrawLayersForPlot, getPlotViewById, hasWCSProjection, - isImageCube, isThreeColor, primePlot, pvEqualExScroll + isCube, isThreeColor, primePlot, pvEqualExScroll } from '../PlotViewUtil.js'; import { ColorButtonIcon, ColorDropDownButton, DistanceButton, DrawLayersButton, ExpandButton, ExtractLine, ExtractPoints, + ExtractTile, FlipYButton, InfoButton, RestoreButton, RotateButton, SaveButton, ToolsDropDown } from './Buttons.jsx'; import {ImageCenterDropDown, TARGET_LIST_PREF} from './ImageCenterDropDown.jsx'; @@ -294,7 +297,9 @@ const ColorButton= ({colorDrops,enabled,pv}) => ( function ToolsDrop({pv,mi, enabled, image, hips, modalEndInfo, showRotateLocked}) { - const showExtract= Boolean(image) && mi.extract; + const plot= primePlot(pv); + const hipsWithFits= isHiPS(plot) && plot.hasFits; + const showExtract= (isImage(plot) || hipsWithFits) && mi.extract; return ( @@ -350,38 +355,46 @@ const RotateFlipRow= ({image,mi,showRotateLocked,pv,enabled}) => ( function startExtraction(element,type,modalEndInfo) { modalEndInfo?.closeLayer?.('Extraction'); let ended= false; - showExtractionDialog(element, type, () => { - if (!ended) setModalEndInfo({}); - }); - setModalEndInfo({ - closeText: 'End Extraction', - key: 'Extraction', - closeLayer: () => { + const wasCanceled= () => !ended && setModalEndInfo({}); + const closeLayer= () => { ended= true; endExtraction(); clearModalEndInfo(); - }, - offOnNewPlot: false - }); + }; + + type===HIPS_TILE + ? showHiPSTileExtractionDialog(element, wasCanceled) + : showExtractionDialog(element, type, wasCanceled); + setModalEndInfo({ closeText: 'End Extraction', key: 'Extraction', offOnNewPlot: false, closeLayer }); } const ExtractRow= ({pv,enabled,modalEndInfo,mi}) => { - const standIm= !isThreeColor(pv); + const plot= primePlot(pv); + const standIm= isImage(plot) && !isThreeColor(pv); + const hipsWithFits= isHiPS(plot) && plot.hasFits; + const cube= standIm && isCube(primePlot(pv)); return ( Extract: - startExtraction(element,Z_AXIS,modalEndInfo)} visible={mi.extractZAxis}/> - startExtraction(element,LINE,modalEndInfo)} visible={mi.extractLine}/> - startExtraction(element,POINTS,modalEndInfo)} visible={mi.extractPoint}/> + {isHiPS(plot) && startExtraction(element,HIPS_TILE,modalEndInfo)} + visible={mi.extractPoint}/> } - ); + ); }; const LayersRow= ({style,image, pv,mi,enabled, modalEndInfo}) => ( diff --git a/src/firefly/js/visualize/ui/extraction/ExtractionDialog.jsx b/src/firefly/js/visualize/ui/extraction/ExtractionDialog.jsx index 9b74d856ba..2909d78974 100644 --- a/src/firefly/js/visualize/ui/extraction/ExtractionDialog.jsx +++ b/src/firefly/js/visualize/ui/extraction/ExtractionDialog.jsx @@ -7,18 +7,12 @@ import {Box, Button, Divider, Stack, Tooltip, Typography} from '@mui/joy'; import {isUndefined} from 'lodash'; import React, {useEffect, useState} from 'react'; import {getAppOptions} from '../../../api/ApiUtil.js'; -import {CHART_RESIZE_DEBOUNCE, wrapResizeMonitor} from '../../../ui/ResizeMonitor'; -import { - dispatchAttachLayerToPlot, dispatchCreateDrawLayer, dispatchDestroyDrawLayer, dispatchDetachLayerFromPlot, - dispatchModifyCustomField -} from '../../DrawLayerDispatch'; -import {makeImagePt} from '../../Point'; import {allowPinnedCharts} from '../../../charts/ChartUtil'; import {ensureDefaultChart} from '../../../charts/ui/ChartsContainer.jsx'; import {pinChart} from '../../../charts/ui/PinnedChartContainer.jsx'; import {downloadChart, PlotlyWrapper} from '../../../charts/ui/PlotlyWrapper.jsx'; -import {dispatchHideDialog, dispatchShowDialog} from '../../../core/ComponentCntlr.js'; -import {dispatchAddActionWatcher, dispatchCancelActionWatcher} from '../../../core/MasterSaga.js'; +import {dispatchShowDialog} from '../../../core/ComponentCntlr.js'; +import {dispatchAddActionWatcher} from '../../../core/MasterSaga.js'; import ExtractLineTool, { addLineDistAttributesToPlots, COLUMN_SELECTION, FREE_SELECTION, LINE_SELECTION } from '../../../drawingLayers/ExtractLineTool.js'; @@ -31,39 +25,39 @@ import {FieldGroup} from '../../../ui/FieldGroup'; import HelpIcon from '../../../ui/HelpIcon.jsx'; import {ListBoxInputField, ListBoxInputFieldView} from '../../../ui/ListBoxInputField.jsx'; import {PopupPanel} from '../../../ui/PopupPanel.jsx'; +import {CHART_RESIZE_DEBOUNCE, wrapResizeMonitor} from '../../../ui/ResizeMonitor'; import {useFieldGroupValue, useStoreConnector} from '../../../ui/SimpleComponent.jsx'; import {ValidationField} from '../../../ui/ValidationField'; import {intValidator} from '../../../util/Validate'; import {CCUtil, CysConverter} from '../../CsysConverter.js'; +import {dispatchAttachLayerToPlot, dispatchCreateDrawLayer, dispatchModifyCustomField} from '../../DrawLayerDispatch'; import {getExtName, hasFloatingData} from '../../FitsHeaderUtil.js'; import {dispatchAttributeChange, dispatchChangePointSelection, dispatchChangePrimePlot} from '../../ImagePlotDispatch'; -import {PLOT_IMAGE} from '../../VisConst'; -import {getDlAry, visRoot} from '../../VisStoreRoots'; import {PlotAttribute} from '../../PlotAttribute.js'; import { convertHDUIdxToImageIdx, currentP, getCubePlaneFromWavelength, getDrawLayerByType, getHDU, getHDUIndex, - getImageCubeIdx, getPlotViewAry, hasWCSProjection, hasWLInfo, isDrawLayerAttached, - isImageCube, isMultiHDUFits, primePlot + getCubePlaneIdx, getPlotViewAry, hasWCSProjection, hasWLInfo, isDrawLayerAttached, isCube, isMultiHDUFits, + primePlot } from '../../PlotViewUtil.js'; +import {makeImagePt} from '../../Point'; +import {PLOT_IMAGE} from '../../VisConst'; +import {getDlAry, visRoot} from '../../VisStoreRoots'; import {computeDistance, computeScreenDistance, getLinePointAry} from '../../VisUtil.js'; import {genPointChartData, genSliceChartData, genZAxisChartData} from './ExtractionChart.jsx'; import {keepDataExtraction, keepZAxisExtraction} from './ExtractionTable.jsx'; +import { + cancelLineExtraction, EXTRACT_DIALOG_ID, endExtraction, EXTRACT_END_ID, ZAXIS_POINT_SELECTION_ID +} from './ExtractionUIUtil'; const CUBE_WARNING=` Values are taken directly from cube plane pixels without spatial interpolation, background subtraction, or PSF corrections. This is a quick-look tool for exploration, not a research-ready product. `; -const DIALOG_ID= 'extractionDialog'; const CHART_ID= 'extractionChart'; -const ZAXIS_POINT_SELECTION_ID= 'z-axisExtraction'; const SELECT_TYPE_TIP= 'Choose mouse selection type: mouse line lock, mouse column lock or free selection'; -export const Z_AXIS= 'Z_AXIS'; -export const LINE= 'LINE'; -export const POINTS= 'POINTS'; - const exTypeCntl= { Z_AXIS: { Panel: ZAxisExtractionPanel, @@ -88,14 +82,12 @@ function enableDrawLayer(typeId) { !isDrawLayerAttached(dl,pv.plotId) && dispatchAttachLayerToPlot(typeId,pv.plotId,true,true, true); } -const EXTRACT_END_ID= 'extractEndId'; - export function showExtractionDialog(element,extractionType,wasCanceled) { endExtraction(); exTypeCntl[extractionType].start(); - DialogRootContainer.defineDialog(DIALOG_ID, , element ); - dispatchShowDialog(DIALOG_ID); + DialogRootContainer.defineDialog(EXTRACT_DIALOG_ID, , element ); + dispatchShowDialog(EXTRACT_DIALOG_ID); dispatchAddActionWatcher( { @@ -106,13 +98,6 @@ export function showExtractionDialog(element,extractionType,wasCanceled) { } -export function endExtraction() { - cancelPointExtraction(); - cancelZaxisExtraction(); - cancelLineExtraction(); - dispatchCancelActionWatcher(EXTRACT_END_ID); -} - function ExtractDialog({extractionType,wasCanceled}) { const {pv, pvCnt} = useStoreConnector( getStoreState); const {canCreateExtractionTable}= getAppOptions().image; @@ -232,8 +217,8 @@ function makeLineExtractionTitle(pv,x1,y1,x2,y2) { const extName= getExtName(plot); hduInfo= (extName || ` HDU #${getHDU(plot)}`) + ' - '; } - if (getImageCubeIdx(plot)>-1) { - cubeInfo= `Plane #${getImageCubeIdx(plot)+1} - `; + if (getCubePlaneIdx(plot)>-1) { + cubeInfo= `Plane #${getCubePlaneIdx(plot)+1} - `; } return `Line Extract Preview - ${hduInfo}${cubeInfo}(${x1},${y1}) to (${x2},${y2})`; } @@ -255,7 +240,7 @@ function PointExtractionPanel({canCreateExtractionTable, pv, pvCnt}) { const {plotId,plotImageId}= plot ?? {}; const hduNum= getHDU(plot); - const plane= getImageCubeIdx(plot)>-1 ? getImageCubeIdx(plot) : 0; + const plane= getCubePlaneIdx(plot)>-1 ? getCubePlaneIdx(plot) : 0; const {x:chartX,y:chartY,chartXAxis:lastChartChartXAxis=chartXAxis}=plot?.attributes?.[PlotAttribute.SELECT_ACTIVE_CHART_PT] ?? {}; useEffect(() => { @@ -319,7 +304,7 @@ function LineExtractionPanel({canCreateExtractionTable, pv, pvCnt}) { const y2= Math.trunc(ipt2?.y ?? 0); const {plotId,plotImageId}= plot ?? {}; const hduNum= getHDU(plot); - const plane= getImageCubeIdx(plot)>-1 ? getImageCubeIdx(plot) : 0; + const plane= getCubePlaneIdx(plot)>-1 ? getCubePlaneIdx(plot) : 0; const {x:chartX,y:chartY}=plot?.attributes?.[PlotAttribute.SELECT_ACTIVE_CHART_PT] ?? {}; @@ -378,13 +363,13 @@ function LineExtractionPanel({canCreateExtractionTable, pv, pvCnt}) { const CubeStartUpHelp= ({plot}) => ( - {isImageCube(plot) ? + {isCube(plot) ? 'Click on a pixel to extract data from all planes of the cube' : 'Please choose a cube to extract z-axis data'} - {isImageCube(plot) && + {isCube(plot) && {CUBE_WARNING} } @@ -581,12 +566,12 @@ function ZAxisExtractionPanel({canCreateExtractionTable, pv}) { useEffect(() => { const updateChart= async () => { if (ipt && plot) { - if (!isImageCube(plot)) { + if (!isCube(plot)) { setChartParams({}); return; } const dataAry = await callGetCubeDrillDownAry(plot, hduNum, ipt, pointSize, combineOp, allRelatedHDUS); - const plane=getImageCubeIdx(plot); + const plane=getCubePlaneIdx(plot); const chartTitle= `Z Axis Preview - ${extName?extName+',':''} HDU #${hduNum}, Point: (${x},${y})`; setChartParams(genZAxisChartData(makeImagePt(x,y), pv, dataAry, plane , dataAry[plane] , pointSize, combineOp, chartTitle)); } @@ -693,27 +678,3 @@ async function keepExtractionAndPin(callKeepExtraction,allowpinChart=false) { } } -function cancelZaxisExtraction() { - dispatchChangePointSelection(ZAXIS_POINT_SELECTION_ID, false); - dispatchHideDialog(DIALOG_ID); -} - -function cancelLineExtraction() { - const {pv}= currentP(); - if (pv) { - dispatchDetachLayerFromPlot(ExtractLineTool.TYPE_ID,pv.plotId,true); - dispatchAttributeChange({plotId:pv.plotId,overlayColorScope:true, - changes:{[PlotAttribute.SELECT_ACTIVE_CHART_PT]: undefined }}); - dispatchDestroyDrawLayer(ExtractLineTool.TYPE_ID); - } - dispatchHideDialog(DIALOG_ID); -} - -function cancelPointExtraction() { - const {pv}= currentP(); - if (pv) { - dispatchDetachLayerFromPlot(ExtractPointsTool.TYPE_ID,pv.plotId,true); - dispatchDestroyDrawLayer(ExtractPointsTool.TYPE_ID); - } - dispatchHideDialog(DIALOG_ID); -} diff --git a/src/firefly/js/visualize/ui/extraction/ExtractionTable.jsx b/src/firefly/js/visualize/ui/extraction/ExtractionTable.jsx index 2265c710d2..b4aee8863c 100644 --- a/src/firefly/js/visualize/ui/extraction/ExtractionTable.jsx +++ b/src/firefly/js/visualize/ui/extraction/ExtractionTable.jsx @@ -15,9 +15,9 @@ import {CCUtil, CysConverter} from '../../CsysConverter'; import {getExtName} from '../../FitsHeaderUtil'; import {visRoot} from '../../VisStoreRoots'; import { - getAllWaveLengthsForCube, getHDU, getHduPlotStartIndexes, getImageCubeIdx, getPlotViewAry, + getAllWaveLengthsForCube, getHDU, getHduPlotStartIndexes, getCubePlaneIdx, getPlotViewAry, getPtWavelength, getWaveLengthUnits, - hasPixelLevelWLInfo, hasWCSProjection, hasWLInfo, isImageCube, isMultiHDUFits, primePlot, + hasPixelLevelWLInfo, hasWCSProjection, hasWLInfo, isCube, isMultiHDUFits, primePlot, } from '../../PlotViewUtil'; import {makeImagePt} from '../../Point'; import {getFluxUnits, isImage} from '../../WebPlot'; @@ -334,7 +334,7 @@ export function makeDataExtractionTable({baseImPtAry, pv, pvAry, extractionSizeX obj['wlUnit'+idx]= getWaveLengthUnits(workingPlot); obj['filename'+idx]= workingPlot.plotState.getWorkingFitsFileStr(); obj['refHDUNum'+idx]= getHDU(workingPlot); - obj['plane'+idx]= getImageCubeIdx(workingPlot)>-1 ? getImageCubeIdx(workingPlot) : 0; + obj['plane'+idx]= getCubePlaneIdx(workingPlot)>-1 ? getCubePlaneIdx(workingPlot) : 0; return obj; },epBase); if (exclusiveToPlot) { @@ -393,7 +393,7 @@ function makePlaneTitle(rootStr, pv, plot, cnt) { if (getExtName(plot)) hduStr = `- ${getExtName(plot)}`; else hduStr = `- HDU#${getHDU(plot)} `; } - if (isImageCube(plot)) cubeStr = `- Plane: ${getImageCubeIdx(plot) + 1}`; + if (isCube(plot)) cubeStr = `- Plane: ${getCubePlaneIdx(plot) + 1}`; return `${rootStr} ${cnt}${hduStr}${cubeStr}`; } diff --git a/src/firefly/js/visualize/ui/extraction/ExtractionUIUtil.js b/src/firefly/js/visualize/ui/extraction/ExtractionUIUtil.js new file mode 100644 index 0000000000..798b10dee4 --- /dev/null +++ b/src/firefly/js/visualize/ui/extraction/ExtractionUIUtil.js @@ -0,0 +1,74 @@ +import {dispatchHideDialog} from '../../../core/ComponentCntlr'; +import {dispatchCancelActionWatcher} from '../../../core/MasterSaga'; +import ExtractHiPSTileTool from '../../../drawingLayers/ExtractHiPSTileTool'; +import ExtractLineTool from '../../../drawingLayers/ExtractLineTool'; +import ExtractPointsTool from '../../../drawingLayers/ExtractPointsTool'; +import {dispatchDestroyDrawLayer, dispatchDetachLayerFromPlot} from '../../DrawLayerDispatch'; +import {dispatchAttributeChange, dispatchChangePointSelection} from '../../ImagePlotDispatch'; +import {PlotAttribute} from '../../PlotAttribute'; +import {currentP, getPlotViewAry, primePlot} from '../../PlotViewUtil'; +import {visRoot} from '../../VisStoreRoots'; +import {isHiPS} from '../../WebPlot'; + + +export const EXTRACT_DIALOG_ID = 'extractionDialog'; +export const HIPS_TILE_EXTRACT_DIALOG_ID = 'hipsTileExtractionDialog'; +export const ZAXIS_POINT_SELECTION_ID = 'z-axisExtraction'; +export const EXTRACT_END_ID = 'extractEndId'; + +export const Z_AXIS = 'Z_AXIS'; +export const LINE = 'LINE'; +export const POINTS = 'POINTS'; +export const HIPS_TILE= 'HIPS_TILE'; + +export function endExtraction() { + cancelPointExtraction(); + cancelZaxisExtraction(); + cancelLineExtraction(); + cancelHiPSTileExtraction(); + dispatchCancelActionWatcher(EXTRACT_END_ID); +} + +export function cancelHiPSTileExtraction() { + const pvAry= getPlotViewAry(visRoot()).filter( (pv) => isHiPS(primePlot(pv))); + dispatchHideDialog(HIPS_TILE_EXTRACT_DIALOG_ID); + pvAry.forEach( (pv) => { + const plotId= pv.plotId; + dispatchDetachLayerFromPlot(ExtractHiPSTileTool.TYPE_ID, plotId); + dispatchAttributeChange({ + plotId, + changes: { + [PlotAttribute.ACTIVE_HIPS_CELL]: undefined, + [PlotAttribute.ACTIVE_HIPS_NORDER]: undefined + } + }); + }); + dispatchDestroyDrawLayer(ExtractHiPSTileTool.TYPE_ID); +} + +function cancelZaxisExtraction() { + dispatchChangePointSelection(ZAXIS_POINT_SELECTION_ID, false); + dispatchHideDialog(EXTRACT_DIALOG_ID); +} + +export function cancelLineExtraction() { + const {pv} = currentP(); + if (pv) { + dispatchDetachLayerFromPlot(ExtractLineTool.TYPE_ID, pv.plotId, true); + dispatchAttributeChange({ + plotId: pv.plotId, + changes: {[PlotAttribute.SELECT_ACTIVE_CHART_PT]: undefined} + }); + dispatchDestroyDrawLayer(ExtractLineTool.TYPE_ID); + } + dispatchHideDialog(EXTRACT_DIALOG_ID); +} + +function cancelPointExtraction() { + const {pv} = currentP(); + if (pv) { + dispatchDetachLayerFromPlot(ExtractPointsTool.TYPE_ID, pv.plotId, true); + dispatchDestroyDrawLayer(ExtractPointsTool.TYPE_ID); + } + dispatchHideDialog(EXTRACT_DIALOG_ID); +} diff --git a/src/firefly/js/visualize/ui/extraction/HiPSTileExtractionDialog.jsx b/src/firefly/js/visualize/ui/extraction/HiPSTileExtractionDialog.jsx new file mode 100644 index 0000000000..f0450488c2 --- /dev/null +++ b/src/firefly/js/visualize/ui/extraction/HiPSTileExtractionDialog.jsx @@ -0,0 +1,145 @@ +import {Stack, Typography} from '@mui/joy'; +import React, {useEffect} from 'react'; +import {dispatchShowDialog} from '../../../core/ComponentCntlr'; +import ExtractHiPSTileTool from '../../../drawingLayers/ExtractHiPSTileTool'; +import {CheckboxGroupInputField} from '../../../ui/CheckboxGroupInputField'; +import {CompleteButton} from '../../../ui/CompleteButton'; +import DialogRootContainer from '../../../ui/DialogRootContainer'; +import {FieldGroup} from '../../../ui/FieldGroup'; +import {PopupPanel} from '../../../ui/PopupPanel'; +import {showInfoPopup} from '../../../ui/PopupUtil'; +import {useFieldValueOnly, useStoreConnector} from '../../../ui/SimpleComponent'; +import {dispatchAttachLayerToPlot, dispatchCreateDrawLayer} from '../../DrawLayerDispatch'; +import {extractFitsFromHiPS} from '../../HiPSUtil'; +import {dispatchChangeActivePlotView, dispatchWcsMatch} from '../../ImagePlotDispatch'; +import {PlotAttribute} from '../../PlotAttribute'; +import {currentP, getDrawLayerByType, getPlotViewAry, isDrawLayerAttached, primePlot} from '../../PlotViewUtil'; +import {WcsMatchType} from '../../VisConst'; +import {getDlAry, visRoot} from '../../VisStoreRoots'; +import {isHiPS} from '../../WebPlot'; +import {endExtraction, HIPS_TILE_EXTRACT_DIALOG_ID} from './ExtractionUIUtil'; + + +export function showHiPSTileExtractionDialog(element, wasCanceled) { + endExtraction(); + const dialog= ; + DialogRootContainer.defineDialog(HIPS_TILE_EXTRACT_DIALOG_ID, dialog, element ); + dispatchShowDialog(HIPS_TILE_EXTRACT_DIALOG_ID); +} + + +function HiPSTileExtractionDialog({wasCanceled}) { + const {pv,plot} = useStoreConnector( () => currentP()); + const hipsPlotCnt = useStoreConnector( + () => getPlotViewAry(visRoot())?.filter( (pv) => isHiPS(primePlot(pv))).length ?? 0); + + const doCancel= () => { + endExtraction(); + wasCanceled?.(); + }; + + useEffect(() => { + startExtraction(pv); + }, []); + + + useEffect(() => { + if (!hipsPlotCnt) doCancel(); + }, [hipsPlotCnt]); + + + const title= isHiPS(plot) + ? `Extract: ${plot?.title ?? ''}` + : 'Not a HiPS image'; + + return( + + + + ); +} + + +function HiPSTileExtractionPanel({pv,plot}) { + return ( + + + + ); +} + + +function TileExtractContent({plot}) { + + const useWcs= useFieldValueOnly('wcsMatch', 'wcs'); + if (!plot) return; + const hipsCell= plot.attributes[PlotAttribute.ACTIVE_HIPS_CELL]; + const norder= plot.attributes[PlotAttribute.ACTIVE_HIPS_NORDER]; + + if (!isHiPS(plot)) { + return ( + + HiPS tile extraction only available for a HiPS display + + ); + } + + return ( + + + Click on anywhere on the HiPS display and click 'Extract Tile' + + + {hipsCell && norder + ? `Extract tile: norder:${norder}, tile:${hipsCell?.ipix??'none'}` + : 'No tile selected' + } + + + {!plot.hasFitsCube && + + Warning: Some FITS tiles do not have valid WCS information + } + extractTile(plot, useWcs==='wcs') } /> + + ); +} + +function extractTile(plot,useWcsMatch) { + if (!plot?.hasFits) return; + + + const hipsCell= plot.attributes[PlotAttribute.ACTIVE_HIPS_CELL]; + if (!hipsCell) { + showInfoPopup('First Click on anywhere on the HiPS display', 'Click on HiPS'); + return; + } + + const cell= plot.attributes[PlotAttribute.ACTIVE_HIPS_CELL]; + const norder= plot.attributes[PlotAttribute.ACTIVE_HIPS_NORDER]; + if (!cell || !norder) return; + extractFitsFromHiPS(plot,norder,cell.ipix); + + if (useWcsMatch) { + setTimeout(() => { + dispatchChangeActivePlotView(plot.plotId); + dispatchWcsMatch({ plotId: plot.plotId, matchType: WcsMatchType.Standard}); + }, 5 ); + } +} + +function startExtraction(pv) { + if (!pv) return; + const typeId= ExtractHiPSTileTool.TYPE_ID; + let extractDl= getDrawLayerByType(getDlAry(), typeId); + if (!extractDl) dispatchCreateDrawLayer(typeId); + extractDl= getDrawLayerByType(getDlAry(),typeId); + !isDrawLayerAttached(extractDl,pv.plotId) && dispatchAttachLayerToPlot(typeId,pv.plotId,true,true, true); +} \ No newline at end of file