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 9da9406cb..f09c6f245 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java @@ -198,6 +198,8 @@ public class ServerParams { public static final String BACK_TO_URL= "backToUrl"; public static final String MASK_DATA= "maskData"; public static final String MASK_BITS= "maskBits"; + public static final String TILE_ACTION= "tileAction"; + public static final String TILE_NUMBER= "tileNumber"; //Workspaces public static final String WS_LIST = "wsList"; // Gets the list of content/files 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 a595102e2..89e9af594 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 @@ -3,6 +3,7 @@ */ package edu.caltech.ipac.firefly.server.rpc; +import edu.caltech.ipac.firefly.core.Util; import edu.caltech.ipac.firefly.data.ServerParams; import edu.caltech.ipac.firefly.server.ServCommand; import edu.caltech.ipac.firefly.server.ServerCommandAccess; @@ -18,17 +19,19 @@ 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; +import jakarta.servlet.http.HttpServletResponse; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.util.List; +import java.util.Map; /** * @author Trey Roby @@ -101,20 +104,29 @@ public static class ByteAryCmd extends ServerCommandAccess.HttpCommand { public void processRequest(HttpServletRequest req, HttpServletResponse res, SrvParam sp) throws Exception { - PlotState state= sp.getState(); - boolean mask= sp.getOptionalBoolean(ServerParams.MASK_DATA,false); - int maskBits= sp.getOptionalInt(ServerParams.MASK_BITS,0); - int tileSize= sp.getRequiredInt(ServerParams.TILE_SIZE); - String compress= sp.getOptional(ServerParams.DATA_COMPRESS, "FULL"); - CompressType ct; - try { - ct = CompressType.valueOf(compress.toUpperCase()); - } catch (IllegalArgumentException e) { - ct= CompressType.FULL; + String tileAction= sp.getRequired(ServerParams.TILE_ACTION); + switch (tileAction) { + case "create" -> createTiles(res, sp); + case "delete" -> deleteTiles(res, sp); + case "getTile" -> getTile(res, sp); + default -> res.sendError(400, "tileAction parameter must be either 'create', 'delete', or 'getTile'. passed: "+tileAction); } + } - byte[] data = VisServerOps.getByteStretchArrayWithUserLocking(state,tileSize,mask,maskBits,ct); - + public void getTile(HttpServletResponse res, SrvParam sp) throws Exception { + PlotState state= sp.getState(); + int tileNumber= sp.getRequiredInt("tileNumber"); + CompressType ct= getCompressType(sp); + var band= sp.contains(ServerParams.BAND) ? Band.parse(sp.getOptional(ServerParams.BAND)) : Band.NO_BAND; + + byte[] data = VisServerOps.getByteStretchTile(state, ct, tileNumber,band); + if (data==null) { + String msg= "Tile Not Found"; + if (!VisServerOps.hasByteStretchDataEntry(state)) msg+= ", No Byte Stretch Data"; + res.sendError(404, msg); + return; + } + res.addHeader("tile-number", tileNumber+""); res.setContentType("application/octet-stream"); ByteBuffer byteBuf = ByteBuffer.wrap(data); byteBuf.position(0); @@ -122,6 +134,48 @@ public void processRequest(HttpServletRequest req, HttpServletResponse res, SrvP chan.write(byteBuf); chan.close(); } + + + public void createTiles(HttpServletResponse res, SrvParam sp) throws Exception { + PlotState state= sp.getState(); + boolean mask= sp.getOptionalBoolean(ServerParams.MASK_DATA,false); + int maskBits= sp.getOptionalInt(ServerParams.MASK_BITS,0); + int tileSize= sp.getRequiredInt(ServerParams.TILE_SIZE); + CompressType ct= getCompressType(sp); + VisServerOps.createByteStretchArrayWithUserLocking(state,tileSize,mask,maskBits,ct); + JSONObject data = new JSONObject(); + var entry= VisServerOps.getByteStretchDataEntry(state); + data.put("tileCount", entry!=null ? entry.getTotalTiles() : 0); + sendJsonDataRet(res,data); + } + + public void deleteTiles(HttpServletResponse res, SrvParam sp) throws Exception { + VisServerOps.deleteByteStretchData(sp.getState(),getCompressType(sp)); + sendJsonDataRet(res,null); + } + + private static void sendJsonDataRet(HttpServletResponse res, Map data) throws IOException { + String jsonData= makeJsonRetData(data); + res.setContentType("application/json"); + res.setContentLength(jsonData.length()); + ServletOutputStream out = res.getOutputStream(); + out.write(jsonData.getBytes()); + out.close(); + } + + private static String makeJsonRetData(Map data) { + JSONArray ary= new JSONArray(); + JSONObject map = new JSONObject(); + map.put("success", true); + map.put("data", data!=null ? data : "none"); + ary.add(map); + return ary.toString(); + } + + private static CompressType getCompressType(SrvParam sp) { + String compress= sp.getOptional(ServerParams.DATA_COMPRESS); + return Util.Try.it(() -> CompressType.valueOf(compress)).getOrElse(CompressType.FULL); + } } diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/DirectStretchUtils.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/DirectStretchUtils.java index 410905f34..c1c4a8cde 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/DirectStretchUtils.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/DirectStretchUtils.java @@ -1,6 +1,5 @@ package edu.caltech.ipac.firefly.server.visualize; -import edu.caltech.ipac.firefly.data.HasSizeOf; import edu.caltech.ipac.firefly.visualize.Band; import edu.caltech.ipac.firefly.visualize.PlotState; import edu.caltech.ipac.visualize.plot.ActiveFitsReadGroup; @@ -14,8 +13,8 @@ import nom.tam.fits.Header; import java.awt.Color; -import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -32,7 +31,7 @@ */ public class DirectStretchUtils { - public enum CompressType {FULL, HALF, HALF_FULL, QUARTER_HALF, QUARTER_HALF_FULL} + public enum CompressType {FULL, HALF, QUARTER, HALF_FULL, QUARTER_HALF, QUARTER_HALF_FULL} private static ExecutorService makeExecutorService () { @@ -60,7 +59,7 @@ public static StretchDataInfo getStretchDataMask(PlotState state, ActiveFitsRead var sTileList= doTileStretch(sv,tileSize, StretchMaskTile::new, (stdef, strContainer) -> () -> strContainer.stretch(stdef, maskList, flip1d,fr.getNaxis1()) ); - byte[] byte1d= combineArray(flip1d.length, sTileList.stream().map( st -> st.result).toList()); + List byte1d= sTileList.stream().map( st -> st.result).toList(); return new StretchDataInfo(byte1d, null, null, getRangeValuesToUse(state)); } @@ -81,41 +80,36 @@ private static StretchDataInfo getStretch3C(PlotState state, ActiveFitsReadGroup FitsRead fr= frGroup.getFitsRead(state.firstBand()); StretchVars sv= getStretchVars(fr,tileSize, ct); RangeValues[] rvAry= getRangeValuesToUse(state); - int bPos= 0; - int bPosHalf=0; - int bPosQuarter=0; Band[] bands= state.getBands(); ThreeCComponents tComp= get3CComponents(frGroup,sv.totWidth,sv.totHeight,state); RGBIntensity rgbI= get3CRGBIntensity(state,frGroup,bands); new ArrayList(sv.tileLen); - var sTileList =doTileStretch(sv,tileSize, Stretch3CTile::new, + List sTileList =doTileStretch(sv,tileSize, Stretch3CTile::new, (stdef,strContainer) -> () -> strContainer.stretch(stdef, rvAry, tComp.float1dAry,tComp.imHeadAry,tComp.histAry,rgbI) ); - int bLen= state.getBands().length; - byte[] byte1d= new byte[sv.totWidth*sv.totHeight * bLen]; - byte[] byte1dHalf= useHalf(ct) ? new byte[dRoundUp(sv.totWidth,2) * dRoundUp(sv.totHeight,2) * bLen] : null; - byte[] byte1dQuarter= useQuarter(ct) ? new byte[dRoundUp(sv.totWidth,4) * dRoundUp(sv.totHeight,4) * bLen] : null; + + var byte1d= useFull(ct) ? new HashMap>() : null; + var byte1dHalf= useHalf(ct) ? new HashMap>() : null; + var byte1dQuarter=useQuarter(ct) ? new HashMap>() : null; for (Stretch3CTile stretchTile : sTileList) { - byte[][] tmpByte3CAry= stretchTile.result; - for(int bandIdx=0; (bandIdx<3);bandIdx++) { - if (tComp.float1dAry[bandIdx]!=null) { - System.arraycopy(tmpByte3CAry[bandIdx],0,byte1d,bPos,tmpByte3CAry[bandIdx].length); - bPos+=tmpByte3CAry[bandIdx].length; - if (useHalf(ct)) { - byte[] hDecimatedAry= stretchTile.resultHalf[bandIdx]; - System.arraycopy(hDecimatedAry, 0, byte1dHalf, bPosHalf, hDecimatedAry.length); - bPosHalf += hDecimatedAry.length; - } - if (useQuarter(ct)) { - byte[] qDecimatedAry= stretchTile.resultQuarter[bandIdx]; - System.arraycopy(qDecimatedAry, 0, byte1dQuarter, bPosQuarter , qDecimatedAry.length); - bPosQuarter += qDecimatedAry.length; - } + for(Band band : bands) { + if (useFull(ct)) { + byte1d.computeIfAbsent(band, k -> new ArrayList<>()) + .add(stretchTile.result[band.getIdx()]); + } + if (useHalf(ct)) { + byte1dHalf.computeIfAbsent(band, k -> new ArrayList<>()) + .add(stretchTile.resultHalf[band.getIdx()]); + } + if (useQuarter(ct)) { + byte1dQuarter.computeIfAbsent(band, k -> new ArrayList<>()) + .add(stretchTile.resultQuarter[band.getIdx()]); } } } - return new StretchDataInfo(byte1d, byte1dHalf, byte1dQuarter, rvAry); + + return new StretchDataInfo(byte1d,byte1dHalf,byte1dQuarter, rvAry); } private static List doTileStretch(StretchVars sv, int tileSize, Callable stretchContainerFactory, @@ -137,21 +131,14 @@ private static List doTileStretch(StretchVars sv, int tileSize, Callable< } private static StretchDataInfo buildStandardResult(List sTileList, RangeValues rv, - int totWidth, int totHeight, CompressType ct) throws Exception { - var taskList= new ArrayList>(); - byte[] byte1d= useFull(ct ) ? new byte[totWidth*totHeight] : null; - byte[] byte1dQuarter= useQuarter(ct) ? new byte[dRoundUp(totWidth,4) * dRoundUp(totHeight,4)]:null; - byte[] byte1dHalf= useHalf(ct) ? new byte[dRoundUp(totWidth,2) * dRoundUp(totHeight,2)]:null; - if (useFull(ct)) { - taskList.add(() -> combineArray(byte1d, sTileList.stream().map( st -> st.result).toList())); - } - if (useQuarter(ct)) { - taskList.add(() -> combineArray(byte1dQuarter, sTileList.stream().map( st -> st.resultQuarter).toList())); - } - if (useHalf(ct)) { - taskList.add(() -> combineArray(byte1dHalf, sTileList.stream().map( st -> st.resultHalf).toList())); - } - invokeList(taskList); + int totWidth, int totHeight, CompressType ct) throws Exception { + List byte1d= useFull(ct) ? + sTileList.stream().map( st -> st.result).toList() : null; + List byte1dHalf= useHalf(ct) ? + sTileList.stream().map( st -> st.resultHalf).toList() : null; + List byte1dQuarter= useQuarter(ct) ? + sTileList.stream().map( st -> st.resultQuarter).toList() : null; + return new StretchDataInfo(byte1d, byte1dHalf, byte1dQuarter, new RangeValues[] {rv}); } @@ -179,18 +166,6 @@ private static boolean useFull(CompressType ct) { return ct==CompressType.FULL || ct==CompressType.HALF_FULL || ct==CompressType.QUARTER_HALF_FULL; } - private static float [] flipFloatArray(float [] float1d, int naxis1, int naxis2) { - float [] flipped= new float[float1d.length]; - int idx=0; - for (int y= naxis2-1; y>=0; y--) { - for (int x= 0; x aList) { - int pos= 0; - for (var dAry : aList) { - System.arraycopy(dAry, 0, target, pos , dAry.length); - pos += dAry.length; - } - return null; - } - - private static byte[] combineArray(int length, List aList) { - byte[] target= new byte[length]; - combineArray(target,aList); - return target; - } - private static int dRoundUp(int v, int factor) { return v % factor == 0 ? v/factor : v/factor +1; } private static byte[] makeDecimated(byte[] in, int factor, int width, int height) { @@ -312,70 +272,7 @@ private record ThreeCComponents(float[][] float1dAry, ImageHeader[] imHeadAry, H private record StretchVars(int totWidth, int totHeight, int xPanels, int yPanels, int tileLen, CompressType ct) {} private record StretchTileDef(int x, int y, int width, int height, CompressType ct) {} - public static class StretchDataInfo implements Serializable, HasSizeOf { - private final byte [] byte1d; - private final byte [] byte1dHalf; - private final byte [] byte1dQuarter; - private final RangeValues[] rvAry; - - public StretchDataInfo(byte[] byte1d, byte[] byte1dHalf, byte[] byte1dQuarter, RangeValues[] rvAry) { - this.byte1d = byte1d; - this.byte1dHalf = byte1dHalf; - this.byte1dQuarter = byte1dQuarter; - this.rvAry= rvAry; - } - - public byte[] findMostCompressAry(CompressType ct) { - return switch (ct) { - case FULL -> byte1d; - case QUARTER_HALF_FULL, QUARTER_HALF -> byte1dQuarter; - case HALF, HALF_FULL -> byte1dHalf; - }; - } - - public static String getMostCompressedDescription(CompressType ct) { - return switch (ct) { - case FULL -> "Full"; - case QUARTER_HALF_FULL, QUARTER_HALF -> "Quarter"; - case HALF, HALF_FULL -> "Half"; - }; - } - - public boolean isRangeValuesMatching(PlotState state) { - if (!state.isThreeColor()) { - return rvAry.length==1 && rvAry[0].toString().equals(state.getRangeValues().toString()); - } - for (Band band : new Band[]{RED, GREEN, BLUE}) { - if (state.isBandUsed(band)) { - int idx= band.getIdx(); - if (rvAry[idx]==null || !rvAry[idx].toString().equals(state.getRangeValues(band).toString())) { - return false; - } - } - } - return true; - } - - /** - * create a version of the object with only the full byte array and optionally the half if the - * CompressType is only using the quarter - * @return a version of StretchDataInfo without all the data we will not use again - */ - public StretchDataInfo copyParts(CompressType ct) { - boolean keepHalf= ct== CompressType.QUARTER_HALF_FULL || ct== CompressType.QUARTER_HALF; - return new StretchDataInfo(byte1d, keepHalf?byte1dHalf:null, null, rvAry); - } - - @Override - public long getSizeOf() { - long sum= rvAry.length * 80L; - if (byte1d!=null) sum+=byte1d.length; - if (byte1dHalf!=null) sum+=byte1dHalf.length; - if (byte1dQuarter!=null) sum+=byte1dQuarter.length; - return sum; - } - } -private static class Stretch3CTile { + private static class Stretch3CTile { byte[][] result; byte[][] resultHalf; byte[][] resultQuarter; diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/StretchDataInfo.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/StretchDataInfo.java new file mode 100644 index 000000000..c4556107f --- /dev/null +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/StretchDataInfo.java @@ -0,0 +1,125 @@ +package edu.caltech.ipac.firefly.server.visualize; + +import edu.caltech.ipac.firefly.data.HasSizeOf; +import edu.caltech.ipac.firefly.visualize.Band; +import edu.caltech.ipac.firefly.visualize.PlotState; +import edu.caltech.ipac.visualize.plot.RangeValues; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +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.RED; + +/** + * + * Container for the byte stretch tile. Holds standard, 3-color, or mask tiles + */ +public class StretchDataInfo implements Serializable, HasSizeOf { + private final Map> byte1d; + private final Map> byte1dHalf; + private final Map> byte1dQuarter; + private final long sizeByteFull; + private final long sizeByteHalf; + private final long sizeByteQuarter; + private final long totalTiles; + private final RangeValues[] rvAry; + + public StretchDataInfo(List byte1d, List byte1dHalf, List byte1dQuarter, RangeValues[] rvAry) { + this( + Collections.singletonMap(Band.NO_BAND, byte1d), + Collections.singletonMap(Band.NO_BAND, byte1dHalf), + Collections.singletonMap(Band.NO_BAND, byte1dQuarter), + rvAry + ); + } + + public StretchDataInfo(Map> byte1d, Map> byte1dHalf, Map> byte1dQuarter, RangeValues[] rvAry) { + this.byte1d = byte1d; + this.byte1dHalf = byte1dHalf; + this.byte1dQuarter = byte1dQuarter; + this.rvAry = rvAry; + sizeByteFull = getSizeOfAryList(byte1d); + sizeByteHalf = getSizeOfAryList(byte1dHalf); + sizeByteQuarter = getSizeOfAryList(byte1dQuarter); + if (byte1d != null) totalTiles = byte1d.values().iterator().next().size(); + else if (byte1dHalf != null) totalTiles = byte1dHalf.values().iterator().next().size(); + else if (byte1dQuarter != null) totalTiles = byte1dQuarter.values().iterator().next().size(); + else totalTiles = 0; + } + + private static long getSizeOfAryList(Map> aryListMap) { + if (aryListMap == null || aryListMap.isEmpty()) return 0; + + long sum = 0; + for (var arrayList : aryListMap.values()) { + if (arrayList != null && !arrayList.isEmpty()) { + sum += arrayList.stream().map((a) -> a.length).mapToLong(Long::valueOf).sum(); + } + } + return sum; + } + + public Map> getFullData(DirectStretchUtils.CompressType ct) { + return switch (ct) { + case FULL -> byte1d; + case QUARTER, QUARTER_HALF_FULL, QUARTER_HALF -> byte1dQuarter; + case HALF, HALF_FULL -> byte1dHalf; + }; + } + + public byte[] findData(DirectStretchUtils.CompressType ct, int tileNumber, Band band) { + return switch (ct) { + case FULL -> byte1d.get(band).get(tileNumber); + case QUARTER, QUARTER_HALF_FULL, QUARTER_HALF -> byte1dQuarter.get(band).get(tileNumber); + case HALF, HALF_FULL -> byte1dHalf.get(band).get(tileNumber); + }; + } + + public boolean hasCompressType(DirectStretchUtils.CompressType ct) { + return switch (ct) { + case FULL -> byte1d != null; + case QUARTER, QUARTER_HALF_FULL, QUARTER_HALF -> byte1dQuarter != null; + case HALF, HALF_FULL -> byte1dHalf != null; + }; + } + + public static String getMostCompressedDescription(DirectStretchUtils.CompressType ct) { + return switch (ct) { + case FULL -> "Full"; + case QUARTER, QUARTER_HALF_FULL, QUARTER_HALF -> "Quarter"; + case HALF, HALF_FULL -> "Half"; + }; + } + + public boolean isRangeValuesMatching(PlotState state) { + if (!state.isThreeColor()) { + return rvAry.length == 1 && rvAry[0].toString().equals(state.getRangeValues().toString()); + } + for (Band band : new Band[]{RED, GREEN, BLUE}) { + if (state.isBandUsed(band)) { + int idx = band.getIdx(); + if (rvAry[idx] == null || !rvAry[idx].toString().equals(state.getRangeValues(band).toString())) { + return false; + } + } + } + return true; + } + + public RangeValues[] getRangeValues() { + return rvAry; + } + + public long getTotalTiles() { + return totalTiles; + } + + @Override + public long getSizeOf() { + return (rvAry.length * 80L) + sizeByteFull + sizeByteHalf + sizeByteQuarter; + } +} 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 acb67d08e..127518a15 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 @@ -9,7 +9,6 @@ import edu.caltech.ipac.firefly.server.util.Logger; import edu.caltech.ipac.firefly.server.util.multipart.UploadFileInfo; import edu.caltech.ipac.firefly.server.visualize.DirectStretchUtils.CompressType; -import edu.caltech.ipac.firefly.server.visualize.DirectStretchUtils.StretchDataInfo; import edu.caltech.ipac.firefly.server.visualize.WebPlotFactory.WebPlotFactoryRet; import edu.caltech.ipac.firefly.visualize.Band; import edu.caltech.ipac.firefly.visualize.BandState; @@ -216,7 +215,7 @@ private static void acquireSemaphore(Semaphore userSemaphore) throws Interrupted } } - public static byte[] getByteStretchArrayWithUserLocking(PlotState state, + public static StretchDataInfo createByteStretchArrayWithUserLocking(PlotState state, int tileSize, boolean mask, long maskBits, @@ -224,7 +223,7 @@ public static byte[] getByteStretchArrayWithUserLocking(PlotState state, Semaphore userSemaphore = getUserSemaphore(); try { acquireSemaphore(userSemaphore); - return getByteStretchArray(state,tileSize,mask,maskBits,ct); + return createByteStretchData(state,tileSize,mask,maskBits,ct); } catch (InterruptedException e) { throw new Exception("Unexpected InterruptedException", e); } finally { @@ -233,31 +232,68 @@ public static byte[] getByteStretchArrayWithUserLocking(PlotState state, } - public static byte[] getByteStretchArray(PlotState state, int tileSize, boolean mask, long maskBits, CompressType ct) { - DirectStretchUtils.StretchDataInfo data; + public static byte[] getByteStretchTile(PlotState state, CompressType ct, int tileNumber, Band band) { + CacheKey stretchDataKey= new StringKey(state.getContextString()+"byte-data"); + StretchDataInfo data= (StretchDataInfo)CacheManager.getVisMemCache().get(stretchDataKey); + if (data == null) { + _log.error("getByteStretchTile: context error, StretchDataInfo not in cache, " + + "tileNumber: " + tileNumber + ", band: " + band.toString() ); + return null; + } + return data.findData(ct, tileNumber,band); + } + + public static boolean hasByteStretchDataEntry(PlotState state) { + return getByteStretchDataEntry(state)!=null; + } + public static StretchDataInfo getByteStretchDataEntry(PlotState state) { + CacheKey stretchDataKey= new StringKey(state.getContextString()+"byte-data"); + return (StretchDataInfo)CacheManager.getVisMemCache().get(stretchDataKey); + } + + + public static void deleteByteStretchData(PlotState state, CompressType ct) { + CacheKey stretchDataKey= new StringKey(state.getContextString()+"byte-data"); + Cache memCache= CacheManager.getVisMemCache(); + StretchDataInfo data= (StretchDataInfo)memCache.get(stretchDataKey); + if (data == null) return; + var full= data.getFullData(CompressType.FULL); + var half= data.getFullData(CompressType.HALF); + var rvAry= data.getRangeValues(); + var newData= switch (ct) { + case FULL -> null; + case QUARTER, QUARTER_HALF_FULL, QUARTER_HALF -> new StretchDataInfo(full, half, null, rvAry); + case HALF, HALF_FULL -> new StretchDataInfo(full, null, null, rvAry); + }; + if (newData == null) memCache.remove(stretchDataKey); + else memCache.put(stretchDataKey, newData); + PlotServUtils.statsLog("byteAry", "delete", ct.toString()); + } + + public static StretchDataInfo createByteStretchData(PlotState state, int tileSize, boolean mask, long maskBits, CompressType ct) { + StretchDataInfo data; try { ActiveFitsReadGroup frGroup= CtxControl.prepare(state); Cache memCache= CacheManager.getVisMemCache(); CacheKey stretchDataKey= new StringKey(state.getContextString()+"byte-data"); data= (StretchDataInfo)memCache.get(stretchDataKey); String fromCache= ""; - if (data!=null && data.isRangeValuesMatching(state) && data.findMostCompressAry(ct)!=null) { - if (ct==CompressType.FULL || ct==CompressType.HALF) memCache.remove(stretchDataKey); // this the two types then this is the last time we need this data + if (data!=null && data.isRangeValuesMatching(state) && data.hasCompressType(ct)) { fromCache= " (from Cache)"; } else { data= !mask ? DirectStretchUtils.getStretchData(state,frGroup,tileSize,ct) : - DirectStretchUtils.getStretchDataMask(state,frGroup,tileSize,maskBits); - if (ct!= CompressType.FULL) memCache.put(stretchDataKey, data.copyParts(ct)); + DirectStretchUtils.getStretchDataMask(state,frGroup,tileSize,maskBits); + memCache.put(stretchDataKey, data); } counters.incrementVis("Byte Data: " + StretchDataInfo.getMostCompressedDescription(ct)); PlotServUtils.statsLog("byteAry", - "total-MB", (float)data.findMostCompressAry(ct).length / StringUtils.MEG, + "total-MB", data.getSizeOf() / StringUtils.MEG, "Type", (state.isThreeColor() ? "3 Color" : "Standard") +" - "+ ct + fromCache); CtxControl.refreshCache(state); - return data.findMostCompressAry(ct); + return data; } catch (Exception e) { - return new byte[] {}; + return null; } } diff --git a/src/firefly/js/data/ServerParams.js b/src/firefly/js/data/ServerParams.js index 7f2e45406..d6bb72e04 100644 --- a/src/firefly/js/data/ServerParams.js +++ b/src/firefly/js/data/ServerParams.js @@ -102,6 +102,8 @@ export const ServerParams = { HIPS_MERGE_PRIORITY: 'mergedListPriority', CUBE: 'cube', CATALOG: 'catalog', + TILE_ACTION: 'tileAction', + TILE_NUMBER: 'tileNumber', GEOSHAPE : 'shape', ROTATION : 'rotation', diff --git a/src/firefly/js/threadWorker/WorkerAccess.js b/src/firefly/js/threadWorker/WorkerAccess.js index 52611bcd9..923794987 100644 --- a/src/firefly/js/threadWorker/WorkerAccess.js +++ b/src/firefly/js/threadWorker/WorkerAccess.js @@ -1,3 +1,4 @@ +import {dispatchPlotProgressUpdate} from '../visualize/ImagePlotCntlr'; import Worker from './firefly-thread.worker.js'; import {uniqueId} from 'lodash'; import {Logger} from '../util/Logger.js'; @@ -33,6 +34,11 @@ function makeWorker(workerKey) { const worker= new Worker(); worker.onmessage= (ev) => { const {success,callKey}= ev.data; + if (ev.data.message) { + const {plotId,messageText,requestKey}= ev.data; + dispatchPlotProgressUpdate(plotId,messageText,false,requestKey); + return; + } if (promiseMap.has(callKey)) { const pResponse= promiseMap.get(callKey); if (!success && isWorkerOutOfMemory(ev.data?.error)) { diff --git a/src/firefly/js/threadWorker/firefly-thread.worker.js b/src/firefly/js/threadWorker/firefly-thread.worker.js index e3e0d5489..617023c99 100644 --- a/src/firefly/js/threadWorker/firefly-thread.worker.js +++ b/src/firefly/js/threadWorker/firefly-thread.worker.js @@ -1,3 +1,4 @@ +import PlotState from '../visualize/PlotState'; import {doRawDataWork} from '../visualize/rawData/ManageRawDataThread.js'; import {RawDataThreadActions} from './WorkerThreadActions.js'; @@ -18,7 +19,17 @@ globalThis.onmessage= (event) => { function handleRawDataActions(action) { const {callKey}= action; - doRawDataWork(action) + let sendStatus= () => undefined; + if (action.payload.plotId && action.payload.plotStateSerialized) { + sendStatus= (messageText) => { + const plotState= PlotState.parse(action.payload.plotStateSerialized); + postMessage({message:true, + messageText, + plotId:action.payload.plotId, + requestKey:plotState.getWebPlotRequest().getRequestKey()}); + }; + } + doRawDataWork({...action,sendStatus}) .then( ({data,transferable}) => postMessage({success:true, ...data, callKey}, transferable) ) .catch( (error) => postMessage({error,callKey, success:false}) ); } \ No newline at end of file diff --git a/src/firefly/js/util/WebUtil.js b/src/firefly/js/util/WebUtil.js index 2fea3517d..a53b29a6a 100644 --- a/src/firefly/js/util/WebUtil.js +++ b/src/firefly/js/util/WebUtil.js @@ -966,7 +966,7 @@ export function callWhileAwaiting(p, whileWaitingFunc, waitUntilMS=0) { * @param {String} url * @param {Object} options * @param {boolean} doValidation - * @param loggerFunc + * @param [loggerFunc] * @return {Promise} */ export async function lowLevelDoFetch(url, options, doValidation, loggerFunc) { diff --git a/src/firefly/js/visualize/ImageViewerLayout.jsx b/src/firefly/js/visualize/ImageViewerLayout.jsx index 8418705cf..15263e173 100644 --- a/src/firefly/js/visualize/ImageViewerLayout.jsx +++ b/src/firefly/js/visualize/ImageViewerLayout.jsx @@ -53,9 +53,6 @@ export const ImageViewerLayout= memo(({ plotView, drawLayersAry, width, height, const plot= primePlot(plotView); const hasPlot= Boolean(plot); const plotShowing= Boolean(viewDim.width && viewDim.height && plot && !plotView.nonRecoverableFail); - const onScreen= !plotShowing || isImageOnScreen(plotView); - const sizeViewable= !plotShowing || isImageSizeViewable(plotView); - const loadingRawData= plotShowing && isImage(plot) && !plot?.tileData && !hasLocalStretchByteData(plot); useEffect(() => { if (width && height) { @@ -131,7 +128,7 @@ export const ImageViewerLayout= memo(({ plotView, drawLayersAry, width, height, return (
- +
); @@ -462,11 +459,15 @@ function sizeChange(previousDim,width,height,viewDim) { } -function MessageArea({pv,plotShowing,onScreen, sizeViewable, loadingRawData}) { +function MessageArea({pv,plotShowing}) { + const plot= primePlot(pv); + const loadingRawData= plotShowing && isImage(plot) && !plot?.tileData && !hasLocalStretchByteData(plot); + const sizeViewable= !plotShowing || isImageSizeViewable(pv); + const onScreen= !plotShowing || isImageOnScreen(pv); if (pv.serverCall==='success' && !pv.nonRecoverableFail) { if (loadingRawData) { return ( - ); } diff --git a/src/firefly/js/visualize/PlotViewUtil.js b/src/firefly/js/visualize/PlotViewUtil.js index 2138ca3a0..620e3536f 100644 --- a/src/firefly/js/visualize/PlotViewUtil.js +++ b/src/firefly/js/visualize/PlotViewUtil.js @@ -228,9 +228,9 @@ export const getOverlayByPvAndId = (ref,plotId,imageOverlayId) => export function removeRawDataByPlotView(pv) { - pv?.plots.forEach( (p) => removeRawData(p.plotImageId)); + pv?.plots.forEach( (p) => removeRawData(p.plotImageId,p.plotState)); pv?.overlayPlotViews?.forEach( (opv) => { - opv.plots.forEach( (p) => p?.plotImageId && removeRawData(p.plotImageId) ); + opv.plots.forEach( (p) => p?.plotImageId && removeRawData(p.plotImageId,p.plotState) ); } ); } diff --git a/src/firefly/js/visualize/rawData/JobRunner.js b/src/firefly/js/visualize/rawData/JobRunner.js new file mode 100644 index 000000000..63c8121e7 --- /dev/null +++ b/src/firefly/js/visualize/rawData/JobRunner.js @@ -0,0 +1,175 @@ +import {isString} from 'lodash'; + + +/** + * @file Will running a set for functions (that return a promise) in a batch style. For example if there are + * 20 networks calls, and you want to run only 4 at a time. + * + */ + +const RESTARTED= 'restarted'; +let jobCnt= 0; +const ABORT_MSG= 'Job Aborted while running'; + +/** + * @param {number} maxRunning + * @return {JobRunnerContext} + */ +export function makeJobRunningContext(maxRunning=2) { + /** @type JobRunnerContext */ + const ctx= { waitingJobs: [], runningJobs: [], fetchWorkerIsRunning: false, + restartRaceResolve: undefined, maxRunning, }; + ctx.restartRace = () => ctx.restartRaceResolve?.(RESTARTED); + // todo: in 2027 this can be replaced with Promise.withResolvers() + ctx.restartRacePromise= () => new Promise((resolve) => ctx.restartRaceResolve = resolve); + ctx.createJobPromise= (f,jobGroupId, abortController) => createJobPromise(ctx, f,jobGroupId, abortController); + ctx.abortJobs= (jobGroupId) => abortJobs(ctx, jobGroupId); + return ctx; +} + + +export function isAbortedError(error) { + const msg= isString(error) ? error.toLowerCase() : (error?.message?.toLowerCase() ?? ''); + return msg.includes('aborted'); +} + +/** + * + * @param {JobRunnerContext} ctx + * @param {Function} f - a function that returns a promise. This is the work for the job + * @param {String} jobGroupId - a string for a group of jobs + * @param {AbortController} [abortController] + * @return {Promise} + */ +async function createJobPromise(ctx, f, jobGroupId, abortController) { + /** @type Job */ + const job= { + f, + started:false, + completed:false, + success:true, + failReason:undefined, + aborted:false, + promise:undefined, + jobGroupId, + abortController, + jobId: `job-${jobCnt}` + }; + jobCnt++; + ctx.waitingJobs.push(job); + ctx.restartRace(); + void fetchWorker(ctx); + + if (job.promise) return await job.promise; + + return new Promise( (resolve, reject) => { + const timeoutId = setInterval(async () => { + if (job.aborted) { + clearInterval(timeoutId); + reject(new Error(ABORT_MSG)); + return; + } + if (!job.promise) return; + clearInterval(timeoutId); + try { + const results = await job.promise; + if (job.aborted) { + reject(new Error('Job Aborted while running')); + return; + } + job.completed = true; + + resolve(results); + } catch (e) { + reject(e); + } + }, 5); + }); +} + + +async function fetchWorker(ctx) { + if (ctx.fetchWorkerIsRunning) return; + ctx.fetchWorkerIsRunning = true; + + while (ctx.waitingJobs.length || ctx.runningJobs.length) { + const promises= ctx.runningJobs.filter( (w) => w.jobManagementPromise).map( (w) => w.jobManagementPromise); + for(let i=promises.length; (i winnerJob.jobId !== job.jobId); + } + } + ctx.fetchWorkerIsRunning = false; +} + +export function abortJobs(ctx,jobGroupId) { + ctx.runningJobs + .filter( (job) => job.jobGroupId===jobGroupId && !job.completed && !job.aborted) + .forEach( (job) => { + job.abortController?.abort(`${jobGroupId}: Job Aborted while running`); + job.aborted = true; + }); + ctx.runningJobs= ctx.runningJobs.filter( (job) => !job.aborted); + ctx.waitingJobs + .filter( (job) => job.jobGroupId===jobGroupId) + .forEach( (job) => { + job.aborted = true; + }); + ctx.waitingJobs= ctx.waitingJobs.filter( (job) => !job.aborted); +} + +async function executeJob(job) { + const promise= job.f(); + job.promise= promise; + job.started=true; + try { + await promise; + } + catch (e) { + job.failReason = e; + job.success= false; + } + job.completed=true; + return job; +} + +/** + * @global + * @public + * @typedef {Object} JobRunnerContext + * + * @summary Context for JobRunner + * + * @prop {Array} waitingJobs + * @prop {Array} runningJobs + * @prop {Boolean} fetchWorkerIsRunning + * @prop restartRaceResolve + * @prop {Number} maxRunning + * @prop {Function} restartRace + * @prop {Function} restartRacePromise + * @prop {Function} createJobPromise + * @prop {Function} abortJobs + */ + +/** + * @typedef {Object} Job + * + * @prop f + * @prop {boolean} started + * @prop {boolean} completed + * @prop {boolean} success + * @prop {string} failReason + * @prop {boolean} aborted + * @prop promise + * @prop {String} jobGroupId + * @prop {AbortController} abortController + * @prop {String} jobId + */ diff --git a/src/firefly/js/visualize/rawData/ManageRawDataThread.js b/src/firefly/js/visualize/rawData/ManageRawDataThread.js index 03695f5fe..0a6a7a189 100644 --- a/src/firefly/js/visualize/rawData/ManageRawDataThread.js +++ b/src/firefly/js/visualize/rawData/ManageRawDataThread.js @@ -1,21 +1,24 @@ import BrowserInfo from '../../util/BrowserInfo'; -import {allBandAry, Band} from '../Band.js'; +import {Band} from '../Band.js'; import {ServerParams} from '../../data/ServerParams.js'; +import {getColorModel} from './ColorTable'; import {getGpuJs, getGpuJsImmediate} from './GpuJsConfig'; +import {isAbortedError, makeJobRunningContext} from './JobRunner'; import {addRawDataToCache, getEntry, getEntryCount, removeRawData} from './RawDataThreadCache.js'; import PlotState from '../PlotState.js'; import {RawDataThreadActions} from '../../threadWorker/WorkerThreadActions.js'; -import {lowLevelDoFetch} from '../../util/WebUtil.js'; +import {AJAX_REQUEST, lowLevelDoFetch, REQUEST_WITH} from '../../util/WebUtil.js'; import { - abortFetch, getRealDataDim, getTransferable, - makeFetchOptions, populateRawImagePixelDataInWorker, shouldUseGpuInWorker, TILE_SIZE + getRealDataDim, getTransferable, + populateRawImagePixelDataInWorker, shouldUseGpuInWorker, + TILE_SIZE, FULL, HALF, HALF_FULL, QUARTER, QUARTER_HALF, QUARTER_HALF_FULL, } from './RawDataCommon.js'; +import {createTileWithGPU} from './RawImageTilesGPU'; const {FETCH_DATA, STRETCH, COLOR, MASK_COLOR, GET_FLUX, REMOVE_RAW_DATA, FETCH_STRETCH_BYTE_DATA, ABORT_FETCH, CLOSE_WHEN_IDLE}= RawDataThreadActions; +const jobRunner= makeJobRunningContext(3); - - -export async function doRawDataWork({type,payload}) { +export async function doRawDataWork({type,payload,sendStatus}) { let scheduleClose= false; if (shouldUseGpuInWorker() && !BrowserInfo.supportsWebGpu() && !getGpuJsImmediate() && payload.rootUrl) { await getGpuJs(payload.rootUrl); // make sure the GPU code is loaded up front @@ -25,17 +28,18 @@ export async function doRawDataWork({type,payload}) { switch (type) { case ABORT_FETCH: return abortFetch(payload); - case FETCH_STRETCH_BYTE_DATA: return fetchByteDataArray(payload); + case FETCH_STRETCH_BYTE_DATA: return fetchByteDataArray(payload,sendStatus); case COLOR: return doColorChange(payload); case MASK_COLOR: return doMaskColorChange(payload); case REMOVE_RAW_DATA: { - void abortFetch(payload); + void jobRunner.abortJobs(payload.plotImageId); + await deleteByteData(payload.cmdSrvUrl,payload.plotImageId, payload.plotStateSerialized, FULL); return {data:{type:REMOVE_RAW_DATA, entryCnt:removeRawData(payload.plotImageId)}}; } case CLOSE_WHEN_IDLE: { if (!scheduleClose) { scheduleClose=true; - doScheduleClose(payload?.workerKey); + doScheduleClose(); } return {data:{success:true, type: RawDataThreadActions.CLOSE_WHEN_IDLE}}; } @@ -60,7 +64,7 @@ function deserialize(payload) { -function doScheduleClose(workerKey) { +function doScheduleClose() { let idleCnt= 0; const id= setInterval( () => { if (!getEntryCount()) idleCnt++; @@ -104,68 +108,38 @@ function convertToBits(ary) { return retAry; } -function convertToUint32(ary) { - const retAry= new Uint32Array(ary.length); - for(let i=0;(i { - if (plotState.isBandUsed(b)) { - rt.pixelData3C[b.value]= allTileAry[tileIdx]; - tileIdx++; - } - else { - rt.pixelData3C[b.value]= new Uint8ClampedArray(dataWidth*dataHeight); - rt.pixelData3C[b.value].fill(0); - } - }); - } - } - else { - rawTileDataGroup.rawTileDataAry.forEach( (rt,idx) => - rt.pixelDataStandard= mask? convertToBits(allTileAry[idx]) : allTileAry[idx]); - } + const {tileResultsAry}= callResults; + const rawTileDataGroup= createRawTileDataGroupStandardPopulated(dataWidth,dataHeight, dataCompress, + colorTableId, nanPixelColor, tileResultsAry); let entry= getEntry(plotImageId); if (!entry) { addRawDataToCache(plotImageId,undefined,undefined,undefined,processHeader); entry= getEntry(plotImageId); } - const {retRawTileDataGroup, localRawTileDataGroup}= - await populateRawImagePixelDataInWorker({rawTileDataGroup, colorTableId, isThreeColor:plotState.isThreeColor(), - mask, maskColor, bias, contrast, rootUrl, nanPixelColor}); - entry.rawTileDataGroup= localRawTileDataGroup; + const retRawTileDataGroup = {...rawTileDataGroup}; + if (!shouldUseGpuInWorker()) { + retRawTileDataGroup.rawTileDataAry= retRawTileDataGroup.rawTileDataAry.map((rt) => + ({ ...rt, pixelData3C: undefined, pixelDataStandard: undefined, })); + } + entry.rawTileDataGroup= rawTileDataGroup; + entry.rawTileDataGroup.rawTileDataAry = rawTileDataGroup.rawTileDataAry.map((rt) => + ({ ...rt, workerBitMapTile:undefined, })); - // logging code - uncomment to log - // const elapse= Date.now()-start; - // const totalLen= allTileAry.reduce((total,tile) => total+tile.length, 0); - // const mbPerSec= (totalLen/MEG) / (elapse/1000); - // console.debug(`${plotImageId}: ${getSizeAsString(totalLen)}, ${elapse} ms, MB/Sec: ${mbPerSec}`); const result= {rawTileDataGroup:retRawTileDataGroup, plotStateSerialized, type: FETCH_STRETCH_BYTE_DATA}; const transferable= getTransferable(result); return {data:result, transferable}; @@ -177,13 +151,18 @@ async function fetchByteDataArray(payload) { } } -function getCompressParam(dataCompress, veryLargeData) { +export async function abortFetch({plotImageId}) { + jobRunner.abortJobs(plotImageId); + return {data:{success:true, type: RawDataThreadActions.ABORT_FETCH}}; +} + +function getCompressParam(dataCompress, veryLargeData=false) { switch (dataCompress) { - case 'FULL': return 'FULL'; - case 'HALF': return dataCompress= veryLargeData ? 'HALF' : 'HALF_FULL'; - case 'QUARTER': return dataCompress= veryLargeData ? 'QUARTER_HALF' : 'QUARTER_HALF_FULL'; + case FULL: return FULL; + case HALF: return dataCompress===veryLargeData ? HALF : HALF_FULL; + case QUARTER: return dataCompress===veryLargeData ? QUARTER_HALF : QUARTER_HALF_FULL; } - return 'FULL'; + return FULL; } /** @@ -194,71 +173,169 @@ function getCompressParam(dataCompress, veryLargeData) { */ /** - * - * @param {String} plotImageId - * @param plotStateSerialized - * @param plotState - * @param {number} dataWidth - * @param {number} dataHeight - * @param {boolean} mask - * @param {number} maskBits - * @param {String} cmdSrvUrl - * @param {String} dataCompress - should be 'FULL' or 'HALF' or 'QUARTER' - * @param {boolean} veryLargeData - if true and dataCompress is 'QUARTER' never request full size + * @param {StretchWorkerActionPayload} payload + * @param {Function} sendStatus * @return {Promise} */ -export async function callStretchedByteData(plotImageId,plotStateSerialized,plotState, dataWidth,dataHeight, - mask,maskBits,cmdSrvUrl, dataCompress= 'FULL', veryLargeData= false) { +export async function callStretchedByteData(payload,sendStatus ) { - const options= makeFetchOptions(plotImageId, { - [ServerParams.COMMAND]: ServerParams.GET_BYTE_DATA, - [ServerParams.TILE_SIZE] : TILE_SIZE, - [ServerParams.STATE] : plotStateSerialized, - [ServerParams.MASK_DATA] : mask, - [ServerParams.MASK_BITS] : maskBits, - [ServerParams.DATA_COMPRESS] : getCompressParam(dataCompress, veryLargeData) - }); + const {plotImageId,plotStateSerialized,plotState, dataWidth,dataHeight, + nanPixelColor,colorTableId, mask=false,maskBits,cmdSrvUrl:url, dataCompress= 'FULL'}= payload; + + const colorModel= !mask && !plotState.isThreeColor() && getColorModel(colorTableId,nanPixelColor, !BrowserInfo.supportsWebGpu()); + const ct= getCompressParam(dataCompress, payload.veryLargeData); + const {options}= makeFetchOptions(plotImageId, + { + ...getBaseByteDataParams(plotStateSerialized,'create',ct), + [ServerParams.MASK_DATA] : mask, + [ServerParams.MASK_BITS] : maskBits, + [ServerParams.TILE_SIZE] : TILE_SIZE, + }, + 'create'); - if (dataCompress!=='FULL' && dataCompress!=='HALF' && dataCompress!=='QUARTER') throw(new Error('dataCompress must be FULL or HALF or QUARTER')); + if (dataCompress!==FULL && dataCompress!==HALF && dataCompress!==QUARTER) throw(new Error('dataCompress must be FULL or HALF or QUARTER')); - const response= await lowLevelDoFetch(cmdSrvUrl, options, false ); + const response= await lowLevelDoFetch(url, options, false ); if (!response.ok) { const message= `Fatal: Error from Server for getStretchedByteData: code: ${response.status}, text: ${response.statusText}`; console.log('callStretchedByteData: '+message); return { success:false, message, allTileAry:[] }; } - const byte1d= new Uint8ClampedArray(await response.arrayBuffer()); - if (!byte1d.length) { - return { - success:false, - message: 'Fatal: No data returned from getStretchedByteData', - allTileAry:[] - }; + else { + const results= await response.json(); + if (!results[0]?.data?.tileCount) { + const message= 'callStretchedByteData: error no tiles created'; + console.log(message); + return { success:false, message, allTileAry:[] }; + } } const {tileSize,xPanels,yPanels, realDataWidth, realDataHeight} = getRealDataDim(dataCompress,dataWidth,dataHeight); - let pos= 0; - let idx=0; - const allTileAry= []; - const colorCnt= plotState.isThreeColor() ? plotState.getBands().length : 1; + let tileNumber=0; + + const promiseAry= []; + let totalTiles=0; + let processedTiles=0; + + const incUpdateCnt= async () => { + processedTiles++; + if ((processedTiles % 4)===0 && totalTiles) { + sendStatus(`${processedTiles} of ${totalTiles}`); + } + }; + + for(let i= 0; i r.value); + const success= !tileResultsAry.some( (r) => Boolean(r?.error || !r)); + const aborted= success ? false : tileResultsAry.some( (r) => r?.aborted); + if (success || !aborted) deleteByteData(url,plotImageId, plotStateSerialized, ct); // don't clean up if aborted since it will probably be overridden anyway, avoids a race condition + return success + ? {success, message:'', tileResultsAry} + : {success, message:'tile retrieve failed,', tileResultsAry:[], aborted}; +} + +function deleteByteData(url, plotImageId, plotStateSerialized, ct) { + const {options}= makeFetchOptions(plotImageId, getBaseByteDataParams(plotStateSerialized,'delete',ct),undefined); + void lowLevelDoFetch(url, options, false ); +} + +/** + * Retrieve and process the tile + * @param obj + * @param obj.tileNumber + * @param obj.colorModel + * @param obj.width + * @param obj.height + * @param obj.payload + * @param obj.incUpdateCnt + * @return {Promise<{pixelDataStandard: ArrayBuffer, workerBitMapTile: HTMLCanvasElement|OffscreenCanvas|ImageBitmap}>} + */ +async function getATile({tileNumber, colorModel, width, height, payload,incUpdateCnt}) { + const {cmdSrvUrl:url, plotImageId, plotState, plotStateSerialized, mask, maskColor, bias=.5, contrast=1, + dataCompress, veryLargeData}= payload; + const isThreeColor = plotState.isThreeColor(); + const doBitmap= shouldUseGpuInWorker(); + const ct= getCompressParam(dataCompress, veryLargeData); + const params= { ...getBaseByteDataParams(plotStateSerialized,'getTile',ct), [ServerParams.TILE_NUMBER]: tileNumber+''}; + + const signalId= 'tile'+tileNumber; + + try { + if (isThreeColor) { + const bandUse= {useRed:plotState.isBandUsed(Band.RED),useGreen:plotState.isBandUsed(Band.RED),useBlue:plotState.isBandUsed(Band.RED)}; + const pixelData3C=[undefined,undefined,undefined]; + const bandAry= plotState.getBands(); + const threeCPromises= []; + for(let i=0; (i fetchTileData(url, options),plotImageId, abortController); +} +function getBaseByteDataParams(plotStateSerialized,tileAction,ct) { + return { + [ServerParams.COMMAND]: ServerParams.GET_BYTE_DATA, + [ServerParams.STATE] : plotStateSerialized, + [ServerParams.TILE_ACTION]: tileAction, + [ServerParams.DATA_COMPRESS] : ct, + }; +} + /** * color change needs to do the following @@ -290,6 +367,18 @@ async function changeLocalRawDataColor(plotImageId, colorTableId, threeColor, bi +export function createRawTileDataGroupStandardPopulated(dataWidth,dataHeight, dataCompress, + colorTableId, nanPixelColor, tileResultsAry) { + const rawTileDataGroup= createRawTileDataGroup(dataWidth,dataHeight, dataCompress, undefined); + rawTileDataGroup.colorTableid= colorTableId; + rawTileDataGroup.nanPixelColor = nanPixelColor; + rawTileDataGroup.rawTileDataAry.forEach( (entry,i) =>{ + entry.pixelDataStandard= tileResultsAry[i].pixelDataStandard; + entry.pixelData3C= tileResultsAry[i].pixelData3C; + entry.workerBitMapTile= tileResultsAry[i].workerBitMapTile; + }); + return rawTileDataGroup; +} @@ -301,17 +390,15 @@ async function changeLocalRawDataColor(plotImageId, colorTableId, threeColor, bi * @param rgbIntensity * @return {RawTileData} */ -export function createRawTileDataGroup(dataWidth,dataHeight, dataCompress='FULL', rgbIntensity) { +export function createRawTileDataGroup(dataWidth,dataHeight, dataCompress=FULL, rgbIntensity) { const {tileSize,xPanels,yPanels, realDataWidth, realDataHeight} = getRealDataDim(dataCompress,dataWidth,dataHeight); const rawTileDataAry= []; - // const yIndexes= []; for(let i= 0; i { + /** + * + * @param {String} plotImageId + * @param {PlotState} plotState + */ + const removeRawData= (plotImageId, plotState) => { const entry= getEntry(plotImageId,false); if (!entry) return; rawDataStore= rawDataStore.filter( (s) => s.plotImageId!==plotImageId); if (entry.initialized) { - const action= {type:RawDataThreadActions.REMOVE_RAW_DATA, workerKey:entry.workerKey, callKey:'', payload:{plotImageId}}; + const action= { + type:RawDataThreadActions.REMOVE_RAW_DATA, + workerKey:entry.workerKey, + callKey:'', + payload:{ + plotStateSerialized: plotState.toJson(false), + plotImageId, + cmdSrvUrl: getCmdSrvSyncURL(), + }}; postToWorker(action).then(({entryCnt}) => { if (entryCnt===0) removeWorker(entry.workerKey); }); diff --git a/src/firefly/js/visualize/rawData/RawDataCommon.js b/src/firefly/js/visualize/rawData/RawDataCommon.js index d0667bfc6..ba4ae47c8 100644 --- a/src/firefly/js/visualize/rawData/RawDataCommon.js +++ b/src/firefly/js/visualize/rawData/RawDataCommon.js @@ -1,15 +1,16 @@ import {isArrayBuffer, once} from 'lodash'; import BrowserInfo from '../../util/BrowserInfo.js'; import {createTileWithGPU} from './RawImageTilesGPU.js'; -import {AJAX_REQUEST, MEG, REQUEST_WITH} from '../../util/WebUtil.js'; +import {MEG} from '../../util/WebUtil.js'; import {getColorModel} from './ColorTable.js'; -import {RawDataThreadActions} from 'firefly/threadWorker/WorkerThreadActions.js'; export const HALF= 'HALF'; export const QUARTER= 'QUARTER'; export const FULL= 'FULL'; +export const HALF_FULL= 'HALF_FULL'; +export const QUARTER_HALF= 'QUARTER_HALF'; +export const QUARTER_HALF_FULL= 'QUARTER_HALF_FULL'; -const abortControllers= new Map(); // map of imagePlotId and AbortController export const TILE_SIZE = 3000; export const MAX_FULL_DATA_SIZE = 1200*MEG; //max size of byte data that can be loaded, file size will be 4x to 8x bigger @@ -26,8 +27,8 @@ export const isOffscreenCanvas= (b) => globalThis.OffscreenCanvas && (b instance export const logGpuState= once(() => { const gpuType= BrowserInfo.supportsWebGpu() ? 'webgpu' : 'gpu.js'; const outStr= shouldUseGpuInWorker() - ? `Images: gpu in worker, gpu: ${gpuType}` - : `Images: gpu in main thread: ${gpuType}`; + ? `Images: gpu available in worker, gpu: ${gpuType}` + : `Images: gpu only available in main thread: ${gpuType}`; console.log(outStr); }); @@ -68,9 +69,9 @@ async function populateTileDataInWorker(obj) { * @return {Promise} */ export async function populateRawImagePixelDataInWorker(obj) { - const {rawTileDataGroup, colorTableId, mask, nanPixelColor, isThreeColor=false}= obj; + const {rawTileDataGroup, colorTableId, mask, nanPixelColor, threeColor=false}= obj; if (shouldUseGpuInWorker()) { - const colorModel = !mask && !isThreeColor && getColorModel(colorTableId,nanPixelColor, !BrowserInfo.supportsWebGpu()); + const colorModel = !mask && !threeColor && getColorModel(colorTableId,nanPixelColor, !BrowserInfo.supportsWebGpu()); const rawTileDataAry = await populateTileDataInWorker({...obj,colorModel}); @@ -92,29 +93,7 @@ export async function populateRawImagePixelDataInWorker(obj) { } } -export function makeFetchOptions(plotImageId, params) { - const options= { - method: 'post', - mode: 'cors', - credentials: 'include', - cache: 'default', - params, - headers: { - [REQUEST_WITH]: AJAX_REQUEST, - } - }; - const ac= globalThis.AbortController && new AbortController(); - if (ac) { - abortControllers.set(plotImageId,ac); - options.signal= ac.signal; - } - return options; -} -export async function abortFetch({plotImageId}) { - abortControllers.get(plotImageId)?.abort(); - return {data:{success:true, type: RawDataThreadActions.ABORT_FETCH}}; -} export function getTransferable(result) { if (!result?.rawTileDataGroup) return []; @@ -167,10 +146,6 @@ export function getRealDataDim( dataCompress, dataWidth, dataHeight) { } -// export function getDataCompress(plotImageId) { -// return getEntry(plotImageId)?.rawTileDataGroup?.dataCompress; -// } - /** * @typedef {Object} RawTileData diff --git a/src/firefly/js/visualize/rawData/RawDataOps.js b/src/firefly/js/visualize/rawData/RawDataOps.js index b115067ab..513356b9f 100644 --- a/src/firefly/js/visualize/rawData/RawDataOps.js +++ b/src/firefly/js/visualize/rawData/RawDataOps.js @@ -332,7 +332,7 @@ export async function loadInitialStretchData(pv, plot, dispatcher) { const oPv= mask ? getOverlayById(pv,imageOverlayId) : undefined; const maskOptions= mask ? {maskColor:oPv?.colorAttributes.color, maskBits: oPv?.maskValue } : undefined; const dataCompress= getFirstDataCompress(plot,mask); - const {success, fatal}= await loadStandardStretchData(workerKey, plot, + const {success, fatal, silentAbort=false}= await loadStandardStretchData(workerKey, plot, {dataCompress, backgroundUpdate:false, checkForPlotUpdate:!mask}, maskOptions); if (plotInvalid()) return; @@ -340,7 +340,7 @@ export async function loadInitialStretchData(pv, plot, dispatcher) { dispatcher({ type: ImagePlotCntlr.BYTE_DATA_REFRESH, payload:{plotId, imageOverlayId, plotImageId}}); } else { - if (fatal) { + if (fatal && !silentAbort) { Logger('RawDataOps').warn(`dispatch to the plot failed on BYTE_DATA_REFRESH: ${dataCompress}`); if (dataCompress!==FULL) { dispatcher({ type: ImagePlotCntlr.PLOT_IMAGE_FAIL, @@ -354,8 +354,10 @@ export async function loadInitialStretchData(pv, plot, dispatcher) { else { Logger('RawDataOps').warn(`non fatal, dispatch the the plot failed on BYTE_DATA_REFRESH: ${dataCompress}`); - dispatcher({ type: ImagePlotCntlr.PLOT_IMAGE_FAIL, - payload:{plotId, description:'Failed: Could not retrieve image render data' }}); + if (!silentAbort) { + dispatcher({ type: ImagePlotCntlr.PLOT_IMAGE_FAIL, + payload:{plotId, description:'Failed: Could not retrieve image render data' }}); + } } } } @@ -438,7 +440,7 @@ async function loadStandardStretchData(workerKey, plot, loadingOptions, maskOpti if (entry.initialized) { if (!backgroundUpdate) entry.dataType= CLEARED; if (entry.loadingPromise) { - postToWorker(makeAbortFetchAction(plotImageId, workerKey)); + await postToWorker(makeAbortFetchAction(plotImageId, workerKey)); } } else { @@ -475,7 +477,7 @@ async function loadStandardStretchData(workerKey, plot, loadingOptions, maskOpti return {success, fatal:false}; } else { clearLocalStretchData(latestPlot); - return {success:false, fatal: stretchResult.fatal}; + return {success:false, fatal: stretchResult.fatal, silentAbort:true}; } } catch (failResult) { if (isWorkerOutOfMemory(failResult.error)) { @@ -483,8 +485,8 @@ async function loadStandardStretchData(workerKey, plot, loadingOptions, maskOpti return {success:false, fatal:true}; } else { - const {success,fatal}= failResult; - return {success, fatal}; + const {success,fatal, aborted=false}= failResult; + return {success, fatal, silentAbort:aborted}; } } } diff --git a/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js b/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js index b61dc2f33..d7824348a 100644 --- a/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js +++ b/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js @@ -67,6 +67,25 @@ export function makeAbortFetchAction(plotImageId, workerKey) { }; } +/** + * @typedef {Object} StretchWorkerActionPayload + * @prop {String} plotImageId + * @prop {String} dataCompress + * @prop {boolean} veryLargeData + * @prop {boolean} mask + * @prop {number} maskBits + * @prop {String} maskColor + * @prop {number} dataWidth + * @prop {number} dataHeight + * @prop {String} plotStateSerialized + * @prop {Object} processHeader + * @prop {String} colorTableId + * @prop {number} bias + * @prop {number} contrast + * @prop {String} nanPixelColor + */ + + /** * @@ -81,7 +100,7 @@ export function makeAbortFetchAction(plotImageId, workerKey) { * @return {WorkerAction} */ export function makeRetrieveStretchByteDataAction(plot, plotState, maskOptions, dataCompress, veryLargeData, workerKey) { - const {plotImageId, colorTableId} = plot; + const {plotImageId, colorTableId, plotId} = plot; const b = plot.plotState.firstBand(); const {processHeader} = plot.rawData.bandData[b.value]; const cleanProcessHeader = {...processHeader, imageCoordSys: processHeader.imageCoordSys.toString()}; @@ -92,6 +111,7 @@ export function makeRetrieveStretchByteDataAction(plot, plotState, maskOptions, type: RawDataThreadActions.FETCH_STRETCH_BYTE_DATA, workerKey, payload: { + plotId, plotImageId, dataCompress, veryLargeData, diff --git a/src/firefly/js/visualize/rawData/RawTileDrawer.js b/src/firefly/js/visualize/rawData/RawTileDrawer.js index 487513268..a3bdcac46 100644 --- a/src/firefly/js/visualize/rawData/RawTileDrawer.js +++ b/src/firefly/js/visualize/rawData/RawTileDrawer.js @@ -1,5 +1,5 @@ import {getEntry} from 'firefly/visualize/rawData/RawDataCache.js'; -import {TILE_SIZE} from 'firefly/visualize/rawData/RawDataCommon.js'; +import {FULL, HALF, TILE_SIZE} from 'firefly/visualize/rawData/RawDataCommon.js'; import {contains, intersects} from 'firefly/visualize/VisUtil.js'; import {createCanvas} from 'firefly/util/WebUtil.js'; @@ -16,8 +16,8 @@ import {createCanvas} from 'firefly/util/WebUtil.js'; */ function writeToCanvas(ctx, zf, rawTileDataAry, x, y, width, height, dataCompress) { ctx.imageSmoothingEnabled = false; - const realTileSize = dataCompress === 'FULL' ? TILE_SIZE : dataCompress === 'HALF' ? TILE_SIZE / 2 : TILE_SIZE / 4; - const factor = dataCompress === 'FULL' ? zf : dataCompress === 'HALF' ? zf * 2 : zf * 4; + const realTileSize = dataCompress === FULL ? TILE_SIZE : dataCompress === HALF ? TILE_SIZE / 2 : TILE_SIZE / 4; + const factor = dataCompress === FULL ? zf : dataCompress === HALF ? zf * 2 : zf * 4; const step = Math.trunc(realTileSize * factor); let id, drawX, drawY; const len = rawTileDataAry.length; diff --git a/src/firefly/js/visualize/reducer/HandlePlotChange.js b/src/firefly/js/visualize/reducer/HandlePlotChange.js index fb2276004..67a2e900c 100644 --- a/src/firefly/js/visualize/reducer/HandlePlotChange.js +++ b/src/firefly/js/visualize/reducer/HandlePlotChange.js @@ -30,7 +30,7 @@ import { matchPlotViewByPositionGroup, getPlotViewIdxById, getPlotGroupIdxById, findPlotGroup, getPlotViewById, findCurrentCenterPoint, getCenterOfProjection, isRotationMatching, hasWCSProjection, isThreeColor, getHDU, getMatchingRotationAngle, isImageCube, - convertImageIdxToHDU + convertImageIdxToHDU, hasLocalStretchByteData } from '../PlotViewUtil.js'; import Point, {parseAnyPt, makeImagePt, makeWorldPt, makeDevicePt} from '../Point.js'; import {UserZoomTypes} from '../ZoomUtil.js'; @@ -838,15 +838,22 @@ function requestLocalData(state, action) { function updatePlotProgress(state,action) { const {plotId, message:plottingStatusMsg, done, requestKey, callSuccess=true, allowBackwardUpdates= false}= action.payload; const plotView= getPlotViewById(state,plotId); + const plot= primePlot(plotView); // validate the update + + if (!plotView) return state; if (requestKey!==plotView.request.getRequestKey()) return state; if (plotView.plottingStatusMsg===plottingStatusMsg) return state; - if (!done && plotView.serverCall!=='working' && !allowBackwardUpdates) return state; + + const tileDataLoading= isImage(plot) && !plot?.tileData && !hasLocalStretchByteData(plot); + if (!done && !tileDataLoading && plotView.serverCall!=='working' && !allowBackwardUpdates) return state; // do the update - const serverCall= done ? callSuccess ? 'success' : 'fail' : 'working'; + + const serverCall= (done || tileDataLoading) ? callSuccess ? 'success' : 'fail' : 'working'; + return {...state,plotViewAry:clonePvAry(state,plotId, {plottingStatusMsg,serverCall})}; } diff --git a/src/firefly/js/visualize/reducer/PlotView.js b/src/firefly/js/visualize/reducer/PlotView.js index 8adbc1d69..4ded8e08c 100644 --- a/src/firefly/js/visualize/reducer/PlotView.js +++ b/src/firefly/js/visualize/reducer/PlotView.js @@ -132,7 +132,7 @@ export function makePlotView(plotId, req, pvOptions= {}) { plots:[], visible: pvOptions.visible ?? true, subHighlight: Boolean(pvOptions.subHighlight ?? false), - request: req && req.makeCopy(), + request: req?.makeCopy(), plottingStatusMsg:'Plotting...', serverCall:'success', // one of 'success', 'working', 'fail' primeIdx: -1,