diff --git a/docs/release-notes.md b/docs/release-notes.md index d0c6e2acea..80e34b4fd8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,8 +5,10 @@ - To Access current (unstable) development - (Version _next_, unreleased) use docker tag: `nightly` ## Version 2021.2 +- 2021.2.4 (July 2021) + - docker tag: `latest`, `release-2021.2`, `release-2021.2.4` - 2021.2.3 (June 2021) - - docker tag: `latest`, `release-2021.2`, `release-2021.2.3` + - docker tag: `release-2021.2.3` - 2021.2.2 (June 2021) - docker tag: `release-2021.2.2` - 2021.2.1 (May 2021) @@ -48,6 +50,11 @@ - Fixed: further refinement to error handling when retrieving TAP error documents - Fixed: Simbad name resolution issue ([Firefly-797](https://github.com/Caltech-IPAC/firefly/pull/1103)) - Fixed: Mouse zoom not working correctly ([Firefly-803](https://github.com/Caltech-IPAC/firefly/pull/1103)) +- 2021.2.4 + - Fixed: Image mask not zooming correctly ([Firefly-810](https://github.com/Caltech-IPAC/firefly/pull/1106)) + - Fixed: HiPS select table _i_ link not working ([Firefly-819](https://github.com/Caltech-IPAC/firefly/pull/1106)) + - Fixed: bias slider not working in 3 color mode ([PR](https://github.com/Caltech-IPAC/firefly/pull/1106)) + - Fixed: boolean table filters not working ([Firefly-805](https://github.com/Caltech-IPAC/firefly/pull/1104)) ##### _Pull Request in this release_ @@ -58,7 +65,7 @@ ## Version 2021.1 - 2021.1.0 (February 2021) - - docker tag: `latest`, `release-2021.1`, `release-2021.1.0` + - docker tag: `release-2021.1`, `release-2021.1.0` - original release 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 972a57397b..0666751b9d 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/data/ServerParams.java @@ -169,6 +169,8 @@ public class ServerParams { public static final String LOGOUT = "CmdLogout"; public static final String TILE_SIZE = "tileSize"; public static final String BACK_TO_URL= "backToUrl"; + public static final String MASK_DATA= "maskData"; + public static final String MASK_BITS= "maskBits"; //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 d6de7a9842..4ef9a6f545 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 @@ -187,8 +187,10 @@ 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); - byte [] byte1D= VisServerOps.getByteStretchArray(state,tileSize); + byte [] byte1D= VisServerOps.getByteStretchArray(state,tileSize,mask,maskBits); res.setContentType("application/octet-stream"); ByteBuffer byteBuf = ByteBuffer.wrap(byte1D); 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 3bf41aea11..740acf1f5a 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 @@ -13,10 +13,14 @@ import edu.caltech.ipac.visualize.plot.Histogram; import edu.caltech.ipac.visualize.plot.ImageData; import edu.caltech.ipac.visualize.plot.ImageHeader; +import edu.caltech.ipac.visualize.plot.ImageMask; import edu.caltech.ipac.visualize.plot.RangeValues; import edu.caltech.ipac.visualize.plot.plotdata.FitsRead; import edu.caltech.ipac.visualize.plot.plotdata.RGBIntensity; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -38,7 +42,8 @@ public class DirectStretchUtils { return flipped; } - public static byte [] getStretchData(PlotState state, ActiveFitsReadGroup frGroup, int tileSize) throws InterruptedException { + public static byte [] getStretchData(PlotState state, ActiveFitsReadGroup frGroup, int tileSize, boolean mask, long maskBits) + throws InterruptedException { FitsRead fr= frGroup.getFitsRead(state.firstBand()); int totWidth= fr.getNaxis1(); @@ -118,6 +123,41 @@ public class DirectStretchUtils { } } + } + else if (mask) { + float [] float1d= fr.getRawFloatAry(); + final float [] flip1d= flipFloatArray(float1d,totWidth,totHeight); + byte1d= new byte[flip1d.length]; + + List masksList= new ArrayList(); + for(int j= 0; (j<31); j++) { + if (((maskBits>>j) & 1) != 0) { + masksList.add(new ImageMask(j,Color.RED)); + } + } + ImageMask[] maskAry= masksList.toArray(new ImageMask[0]); + + + for(int i= 0; i im.stretchMaskAndSave(flip1d,fr.getNaxis1())); + } + } + executor.shutdown(); + normalTermination= executor.awaitTermination(600, TimeUnit.SECONDS); + for (ImageData imageData : imageDataAry) { + byte[] tmpByteAry = imageData.getSavedStandardStretch(); + System.arraycopy(tmpByteAry, 0, byte1d, bPos, tmpByteAry.length); + bPos += tmpByteAry.length; + } + } else { float [] float1d= fr.getRawFloatAry(); 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 94268e5385..7d27e207e4 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 @@ -406,12 +406,12 @@ public static float[] getFloatDataArray(PlotState state, Band band) { - public static byte[] getByteStretchArray(PlotState state, int tileSize) { + public static byte[] getByteStretchArray(PlotState state, int tileSize, boolean mask, long maskBits) { try { long start = System.currentTimeMillis(); ActiveCallCtx ctx = CtxControl.prepare(state); ActiveFitsReadGroup frGroup= ctx.getFitsReadGroup(); - byte [] byte1d= DirectStretchUtils.getStretchData(state,frGroup,tileSize); + byte [] byte1d= DirectStretchUtils.getStretchData(state,frGroup,tileSize,mask, maskBits); long elapse = System.currentTimeMillis() - start; PlotServUtils.statsLog("byteAry", "total-MB", (float)byte1d.length / StringUtils.MEG, diff --git a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSMasterList.java b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSMasterList.java index fa83dd8599..3cda71add7 100644 --- a/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSMasterList.java +++ b/src/firefly/java/edu/caltech/ipac/firefly/server/visualize/hips/HiPSMasterList.java @@ -195,7 +195,7 @@ private void setupMeta(DataGroup dg, boolean bMulti) { col.setWidth(4); col.setFilterable(false); col.setSortable(false); - col.setLinkInfos(Collections.singletonList(new LinkInfo(null, INFO_ICON_STUB, null, "link to HiPS properties", null, null, null))); + col.setLinkInfos(Collections.singletonList(new LinkInfo(null, INFO_ICON_STUB, "${Properties}", "link to HiPS properties", null, null, null))); } private static DataGroup createTableDataFromListEntry(List hipsMaps) { diff --git a/src/firefly/java/edu/caltech/ipac/visualize/plot/ImageData.java b/src/firefly/java/edu/caltech/ipac/visualize/plot/ImageData.java index 92a57963e4..ee982fe28f 100644 --- a/src/firefly/java/edu/caltech/ipac/visualize/plot/ImageData.java +++ b/src/firefly/java/edu/caltech/ipac/visualize/plot/ImageData.java @@ -266,10 +266,24 @@ public byte[] stretch8bit(final float [] float1d, final Header header, final His return byteAry; } + public byte[] stretchMask(final float [] float1d, final int naxis1) { + byte [] byteAry= new byte[this.width * this.height]; + byte blank_pixel_value = (byte) 255; + int[] pixelhist = new int[256]; + + ImageStretch.stretchPixelsForMask(x, lastPixel, y, lastLine, naxis1, + blank_pixel_value, float1d, byteAry, pixelhist, imageMasks); + return byteAry; + } + public void stretch8bitAndSave(final float [] float1d, final Header header, final Histogram histogram) { this.saveStandardStretch = stretch8bit(float1d,header,histogram); } + public void stretchMaskAndSave(final float [] float1d, final int naxis1) { + this.saveStandardStretch = stretchMask(float1d,naxis1); + } + public byte[] getSavedStandardStretch() { return this.saveStandardStretch; } public void stretch3ColorAndSave(float [][] float1dAry, ImageHeader [] imHeadAry, Histogram[] histAry, diff --git a/src/firefly/js/data/ServerParams.js b/src/firefly/js/data/ServerParams.js index 25061ea3c7..6ae7d556de 100644 --- a/src/firefly/js/data/ServerParams.js +++ b/src/firefly/js/data/ServerParams.js @@ -65,6 +65,8 @@ export const ServerParams = { SCROLL_Y: 'scrollY', ZOOM_FACTOR: 'zoomFactor', RANGE_VALUES: 'rangeValues', + MASK_DATA: 'maskData', + MASK_BITS: 'maskBits', EXT_TYPE: 'extType', IMAGE: 'image', diff --git a/src/firefly/js/util/Color.js b/src/firefly/js/util/Color.js index e352929560..22f77d013c 100644 --- a/src/firefly/js/util/Color.js +++ b/src/firefly/js/util/Color.js @@ -45,7 +45,7 @@ function toRGBA(/* String */ color, /* Number */ alpha=1) { } } -function toRGB(/* String */ color) { +export function toRGB(/* String */ color) { if (color.startsWith('rgb')) { const intColor= toRGBA(color); intColor.length=3; diff --git a/src/firefly/js/util/WebUtil.js b/src/firefly/js/util/WebUtil.js index db08a6909d..26dc149e3b 100644 --- a/src/firefly/js/util/WebUtil.js +++ b/src/firefly/js/util/WebUtil.js @@ -69,7 +69,7 @@ export const isOffscreenCanvas= (b) => getGlobalObj().OffscreenCanvas && (b inst export function createCanvas(width,height) { const global= getGlobalObj(); - const c = global.document ? global.document.createElement('canvas') : new OffscreenCanvas(w,h); + const c = global.document ? global.document.createElement('canvas') : new OffscreenCanvas(width,height); c.width = width; c.height = height; return c; diff --git a/src/firefly/js/visualize/ImagePlotCntlr.js b/src/firefly/js/visualize/ImagePlotCntlr.js index 05d5949530..8f2f2b8644 100644 --- a/src/firefly/js/visualize/ImagePlotCntlr.js +++ b/src/firefly/js/visualize/ImagePlotCntlr.js @@ -939,8 +939,8 @@ export function dispatchUseTableAutoScroll(useAutoScroll) { flux.process({ type: USE_TABLE_AUTO_SCROLL, payload: {useAutoScroll} }); } -export function dispatchRequestLocalData({plotId, plotImageId, dataRequested=true}) { - flux.process({ type: REQUEST_LOCAL_DATA, payload: {plotId,plotImageId, dataRequested} }); +export function dispatchRequestLocalData({plotId, plotImageId, imageOverlayId, dataRequested=true}) { + flux.process({ type: REQUEST_LOCAL_DATA, payload: {plotId,plotImageId, dataRequested, imageOverlayId} }); } /** diff --git a/src/firefly/js/visualize/ImageViewerLayout.jsx b/src/firefly/js/visualize/ImageViewerLayout.jsx index 5642292098..5b52ffc6fd 100644 --- a/src/firefly/js/visualize/ImageViewerLayout.jsx +++ b/src/firefly/js/visualize/ImageViewerLayout.jsx @@ -112,13 +112,29 @@ const rootStyle= { overflow:'hidden' }; -function getDataIfNecessary(pv) { +function getPlotImageToRequest(pv) { const plot= primePlot(pv); - if (!plot || plot.dataRequested) return; + if (!plot) return undefined; const {viewDim:{width,height}}= pv; - if (!width || !height) return; - const {plotImageId,plotId}= plot; - dispatchRequestLocalData({plotId,plotImageId}); + if (!width || !height) return undefined; + if (plot.dataRequested && !pv.overlayPlotViews?.length) return undefined; + if (!plot.dataRequested) return {plotImageId:plot.plotImageId}; + if (pv.overlayPlotViews?.length) { + const overPv= pv.overlayPlotViews.find( (oPv) => oPv?.plot?.dataRequested===false); + if (!overPv) return undefined; + return {plotImageId:overPv.plot.plotImageId, imageOverlayId:overPv.imageOverlayId}; + } + return undefined; + +} + +function getDataIfNecessary(pv) { + const result= getPlotImageToRequest(pv); + if (!result) return; + const plot= primePlot(pv); + if (!plot) return; + const {plotId}= plot; + dispatchRequestLocalData({plotId,plotImageId:result.plotImageId, imageOverlayId:result.imageOverlayId}); } export class ImageViewerLayout extends PureComponent { diff --git a/src/firefly/js/visualize/PlotViewUtil.js b/src/firefly/js/visualize/PlotViewUtil.js index 6c94e4d2f4..22aa8b57b1 100644 --- a/src/firefly/js/visualize/PlotViewUtil.js +++ b/src/firefly/js/visualize/PlotViewUtil.js @@ -13,7 +13,7 @@ import {getWavelength, isWLAlgorithmImplemented, PLANE} from './projection/Wavel import {getNumberHeader, HdrConst} from './FitsHeaderUtil.js'; import {computeDistance, getRotationAngle, isCsysDirMatching, isEastLeftOfNorth, isPlotNorth} from './VisUtil'; import {removeRawData} from './rawData/RawDataCache.js'; -import {MAX_DIRECT_IMAGE_SIZE, MAX_RAW_IMAGE_SIZE} from './rawData/RawDataCommon.js'; +import {MAX_DIRECT_IMAGE_SIZE, MAX_DIRECT_MASK_IMAGE_SIZE, MAX_RAW_IMAGE_SIZE} from './rawData/RawDataCommon.js'; import {hasClearedDataInStore, hasLocalRawDataInStore, hasLocalStretchByteDataInStore} from './rawData/RawDataOps.js'; @@ -227,7 +227,10 @@ export const getOverlayByPvAndId = (ref,plotId,imageOverlayId) => -export const removeRawDataByPlotView= (pv) => pv?.plots.forEach( (p) => removeRawData(p.plotImageId)); +export function removeRawDataByPlotView(pv) { + pv?.plots.forEach( (p) => removeRawData(p.plotImageId)); + pv?.overlayPlotViews?.forEach( (oPv) => oPv?.plot?.plotImageId && removeRawData(oPv.plot.plotImageId) ); +} /** * construct an array of drawing layer from the store @@ -1099,10 +1102,12 @@ export function canLoadStretchData(plot) { return (plot.dataWidth*plot.dataHeight) < MAX_RAW_IMAGE_SIZE; } -export function canLoadStretchDataDirect(plot) { +export function canLoadStretchDataDirect(plot, mask=false) { if (!plot || !isImage(plot)) return false; const size= (plot.dataWidth*plot.dataHeight); - return size rt.pixelDataStandard= allTileAry[idx]); + if (mask) { + rawTileDataGroup.rawTileDataAry.forEach( (rt,idx) => rt.pixelDataStandard= convertToBits(allTileAry[idx])); + } + else { + rawTileDataGroup.rawTileDataAry.forEach( (rt,idx) => rt.pixelDataStandard= allTileAry[idx]); + } } let entry= getEntry(plotImageId); if (!entry) { @@ -100,7 +116,8 @@ async function fetchByteDataArray(payload) { entry= getEntry(plotImageId); } const {retRawTileDataGroup, localRawTileDataGroup}= - await populateRawImagePixelDataInWorker(rawTileDataGroup, plotState.colorTableId, plotState.isThreeColor(), bias, contrast, {}, rootUrl); + await populateRawImagePixelDataInWorker(rawTileDataGroup, plotState.colorTableId, plotState.isThreeColor(), + mask, maskColor, bias, contrast, {}, rootUrl); entry.rawTileDataGroup= localRawTileDataGroup; @@ -120,12 +137,14 @@ async function fetchByteDataArray(payload) { } -export async function callStretchedByteData(plotImageId,plotStateSerialized,plotState, dataWidth,dataHeight,cmdSrvUrl) { +export async function callStretchedByteData(plotImageId,plotStateSerialized,plotState, dataWidth,dataHeight,mask,maskBits,cmdSrvUrl) { const options= makeFetchOptions(plotImageId, { [ServerParams.COMMAND]: ServerParams.GET_BYTE_DATA, [ServerParams.STATE] : plotStateSerialized, [ServerParams.TILE_SIZE] : TILE_SIZE, + [ServerParams.MASK_DATA] : mask, + [ServerParams.MASK_BITS] : maskBits, }); @@ -184,7 +203,8 @@ async function changeLocalRawDataColor(plotImageId, colorTableId, threeColor, bi const newPlotState = plotState.copy(); newPlotState.colorTableId = colorTableId; const {retRawTileDataGroup, localRawTileDataGroup}= - await populateRawImagePixelDataInWorker(entry.rawTileDataGroup, colorTableId, threeColor, bias, contrast, bandUse, rootUrl); + await populateRawImagePixelDataInWorker(entry.rawTileDataGroup, colorTableId, threeColor, false, '', + bias, contrast, bandUse, rootUrl); entry.rawTileDataGroup= localRawTileDataGroup; return {rawTileDataGroup:retRawTileDataGroup, plotStateSerialized: newPlotState.toJson(false)}; } diff --git a/src/firefly/js/visualize/rawData/RawDataCommon.js b/src/firefly/js/visualize/rawData/RawDataCommon.js index f4630c4d8b..e29688f2ab 100644 --- a/src/firefly/js/visualize/rawData/RawDataCommon.js +++ b/src/firefly/js/visualize/rawData/RawDataCommon.js @@ -16,19 +16,20 @@ const abortControllers= new Map(); // map of imagePlotId and AbortController export const TILE_SIZE = 3000; export const MAX_RAW_IMAGE_SIZE = 400*MEG; // 400 megs export const MAX_DIRECT_IMAGE_SIZE= 250*K; +export const MAX_DIRECT_MASK_IMAGE_SIZE= 200*MEG; const USE_GPU = true; -async function populateRawTileDataArray(rawTileDataAry, colorModel, isThreeColor, bias, contrast, bandUse, rootUrl) { +async function populateRawTileDataArray(rawTileDataAry, colorModel, isThreeColor, mask, maskColor, bias, contrast, bandUse, rootUrl) { const GPU = USE_GPU ? await getGpuJs(rootUrl) : undefined; const createTransitionalTile = USE_GPU ? getGPUOps(GPU).createTransitionalTileWithGPU : createTransitionalTileWithCPU; - const presult = rawTileDataAry.map((id) => createTransitionalTile(id, colorModel, isThreeColor, bias, contrast, bandUse)); + const presult = rawTileDataAry.map((id) => createTransitionalTile(id, colorModel, isThreeColor, mask, maskColor, bias, contrast, bandUse)); return await Promise.all(presult); } -export async function populateRawImagePixelDataInWorker(rawTileDataGroup, colorTableId, isThreeColor, bias, contrast, bandUse, rootUrl) { - if (isGPUAvailableInWorker()) { +export async function populateRawImagePixelDataInWorker(rawTileDataGroup, colorTableId, isThreeColor, mask, maskColor, bias, contrast, bandUse, rootUrl) { + if (isGPUAvailableInWorker() && !mask) { const colorModel = getColorModel(colorTableId); - const rawTileDataAry = await populateRawTileDataArray(rawTileDataGroup.rawTileDataAry, colorModel, isThreeColor, bias, contrast, bandUse, rootUrl); + const rawTileDataAry = await populateRawTileDataArray(rawTileDataGroup.rawTileDataAry, colorModel, isThreeColor, mask, maskColor, bias, contrast, bandUse, rootUrl); const localRawTileDataGroup = {...rawTileDataGroup, rawTileDataAry, colorTableId}; diff --git a/src/firefly/js/visualize/rawData/RawDataOps.js b/src/firefly/js/visualize/rawData/RawDataOps.js index 16c9373bb5..bea2bbce4e 100644 --- a/src/firefly/js/visualize/rawData/RawDataOps.js +++ b/src/firefly/js/visualize/rawData/RawDataOps.js @@ -1,7 +1,7 @@ import {isArray, isArrayBuffer, uniqueId} from 'lodash'; import {allBandAry, Band} from '../Band.js'; import {contains, intersects} from '../VisUtil.js'; -import {findPlot, getPlotViewById, isThreeColor, primePlot} from '../PlotViewUtil.js'; +import {findPlot, getOverlayById, getPlotViewById, isThreeColor, primePlot} from '../PlotViewUtil.js'; import {createCanvas, isGPUAvailableInWorker, isImageBitmap} from '../../util/WebUtil.js'; import ImagePlotCntlr, {dispatchRequestLocalData, visRoot} from '../ImagePlotCntlr.js'; import {PlotState} from '../PlotState.js'; @@ -39,16 +39,17 @@ function createTileFromImageData(buffer, width,height) { -function* rawTileGenerator(rawTileDataGroup, colorTableId, bias, contrast, bandUse, GPU) { +function* rawTileGenerator(rawTileDataGroup, colorTableId, mask, maskColor, bias, contrast, bandUse, GPU) { const {rawTileDataAry}= rawTileDataGroup; const newRawTileDataAry= []; + const gpu= getGPUOps(GPU); for(let i=0; (i isArrayBuffer(a)) ) { - tile= getGPUOps(GPU).createTileWithGPU(rawTileDataAry[i],getColorModel(colorTableId),isArray(pixelData3C), bias, contrast,bandUse); + tile= gpu.createTileWithGPU(rawTileDataAry[i],getColorModel(colorTableId),isArray(pixelData3C), mask, maskColor, bias, contrast,bandUse); } else { tile= createTileFromImageData(workerTmpTile, width,height); @@ -60,10 +61,10 @@ function* rawTileGenerator(rawTileDataGroup, colorTableId, bias, contrast, bandU } -async function populateTilesAsync(rawTileDataGroup, colorTableId,bias,contrast, bandUse) { +async function populateTilesAsync(rawTileDataGroup, colorTableId,mask, maskColor, bias,contrast, bandUse) { const chunkSize= 5; const GPU= await getGpuJs(); - const gen= rawTileGenerator(rawTileDataGroup,colorTableId, bias, contrast, bandUse, GPU); + const gen= rawTileGenerator(rawTileDataGroup,colorTableId, mask, maskColor, bias, contrast, bandUse, GPU); return new Promise((resolve, reject) => { const id= setInterval( () => { @@ -235,7 +236,7 @@ export async function changeLocalRawDataColor(plot, colorTableId, bias, contrast plotStateSerialized= newPlotState.toJson(false); rawTileDataGroup= entry.rawTileDataGroup; } - entry.rawTileDataGroup = await populateTilesAsync(rawTileDataGroup, colorTableId, bias,contrast, bandUse); + entry.rawTileDataGroup = await populateTilesAsync(rawTileDataGroup, colorTableId, undefined, undefined, bias,contrast, bandUse); entry.thumbnailEncodedImage = makeThumbnailCanvas(plot); entry.colorChangingInProgress= false; const localScreenTileDefList = getLocalScreenTileDefList(); @@ -262,6 +263,14 @@ export function changeLocalRawDataZoom(plot, zoomFactor) { return {localScreenTileDefList: getLocalScreenTileDefList(), plotState: newPlotState}; } +export async function changeLocalMaskColor(plot, maskColor) { + if (!getEntry(plot.plotImageId)) return; + const newPlotState = plot.plotState.copy(); + const entry = getEntry(plot.plotImageId); + entry.rawTileDataGroup = await populateTilesAsync(entry.rawTileDataGroup, 0, true, maskColor); + return {localScreenTileDefList: getLocalScreenTileDefList(), plotState: newPlotState}; +} + /** * @param {WebPlot} plot @@ -316,17 +325,23 @@ export function clearLocalStretchData(plot) { } - - -export function loadStretchData(plotOrAry, dispatcher) { - const plotAry= isArray(plotOrAry) ? plotOrAry : [plotOrAry]; +export function loadStretchData(pv, plot, dispatcher) { + const plotAry= [plot]; plotAry.forEach( (p) => { const workerKey= getEntry(p.plotImageId)?.workerKey ?? getNextWorkerKey(); - const {plotImageId, plotId}= p; - loadStandardStretchData(p, true, workerKey) + const {plotImageId}= p; + const imageOverlayId= p.plotId!==pv.plotId ? p.plotId : undefined; // i have an overlay image + const mask= Boolean(imageOverlayId); + const oPv= mask ? getOverlayById(pv,imageOverlayId) : undefined; + const maskColor= mask ? oPv?.colorAttributes.color : undefined; + const maskBits= mask ? oPv?.maskValue : undefined; + loadStandardStretchData(p, !mask, mask, maskColor, maskBits, workerKey) .then( (rawData) => - rawData && dispatcher({type: ImagePlotCntlr.UPDATE_RAW_IMAGE_DATA, payload:{plotId, plotImageId, rawData}}) + rawData && dispatcher({ + type: ImagePlotCntlr.UPDATE_RAW_IMAGE_DATA, + payload:{plotId:pv.plotId, imageOverlayId, plotImageId, rawData + }}) ); }); } @@ -344,7 +359,7 @@ export async function stretchRawData(plot, rvAry) { } -async function loadStandardStretchData( plot, checkForPlotUpdate, workerKey) { +async function loadStandardStretchData( plot, checkForPlotUpdate, mask, maskColor, maskBits, workerKey) { const {processHeader} = plot.rawData.bandData[0]; const {plotImageId, plotId}= plot; let entry = getEntry(plotImageId); @@ -360,7 +375,8 @@ async function loadStandardStretchData( plot, checkForPlotUpdate, workerKey) { } entry.loadingCnt++; try { - const stretchResult = await postToWorker(makeRetrieveStretchByteDataAction(plot, plot.plotState, workerKey)); + const stretchResult = await postToWorker( + makeRetrieveStretchByteDataAction(plot, plot.plotState, mask, maskBits, maskColor, workerKey)); entry.loadingCnt--; if (!stretchResult.success) return; @@ -374,13 +390,13 @@ async function loadStandardStretchData( plot, checkForPlotUpdate, workerKey) { continueLoading = plotState.getBands().every((b) => plotState.getRangeValues(b)?.toJSON() === plot.plotState.getRangeValues(b)?.toJSON()); } else { - latestPlot= plot; continueLoading= true; + latestPlot= plot; } if (continueLoading) { entry.dataType = STRETCH_ONLY; - return completeLoad(latestPlot, stretchResult); + return mask ? completeMaskLoad(latestPlot, stretchResult, maskColor) : completeLoad(latestPlot, stretchResult); //todo - get mask color } else { clearLocalStretchData(latestPlot); } @@ -407,6 +423,17 @@ async function completeLoad(plot, stretchResult) { } +async function completeMaskLoad(plot, stretchResult, maskColor) { + const {rawTileDataGroup, plotStateSerialized} = stretchResult; + const plotState = PlotState.parse(plotStateSerialized); + const entry = getEntry(plot.plotImageId); + entry.rawTileDataGroup = await populateTilesAsync(rawTileDataGroup, 0, true, maskColor); + const localScreenTileDefList = getLocalScreenTileDefList(); + return {...plot.rawData, plotState, localScreenTileDefList}; +} + + + //----------------------------------------------------------------------- //----------------------------------------------------------------------- diff --git a/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js b/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js index b402428c17..e4701b6ea8 100644 --- a/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js +++ b/src/firefly/js/visualize/rawData/RawDataThreadActionCreators.js @@ -99,9 +99,12 @@ export function makeLoadAction(plot, band, workerKey) { * @param {WebPlot} plot * @param plotState * @param {String} workerKey + * @param {boolean} mask + * @param {number} maskBits + * @param {string} maskColor * @return {WorkerAction} */ -export function makeRetrieveStretchByteDataAction(plot, plotState, workerKey) { +export function makeRetrieveStretchByteDataAction(plot, plotState, mask, maskBits, maskColor, workerKey) { const {plotImageId} = plot; const b = plot.plotState.firstBand(); const {processHeader} = plot.rawData.bandData[b.value]; @@ -113,6 +116,9 @@ export function makeRetrieveStretchByteDataAction(plot, plotState, workerKey) { workerKey, payload: { plotImageId, + mask, + maskBits, + maskColor, dataWidth: plot.dataWidth, dataHeight: plot.dataHeight, plotStateSerialized: plotState.toJson(false), diff --git a/src/firefly/js/visualize/rawData/RawFloatData.js b/src/firefly/js/visualize/rawData/RawFloatData.js index 6ac4e214c2..6ef12a4e49 100644 --- a/src/firefly/js/visualize/rawData/RawFloatData.js +++ b/src/firefly/js/visualize/rawData/RawFloatData.js @@ -127,7 +127,7 @@ async function changeLocalRawDataStretch(plotImageId, dataWidth, dataHeight, plo plotState.bandStateAry[bIdx].rangeValuesSerialize = rv.toJSON(); } const {retRawTileDataGroup, localRawTileDataGroup}= - await populateRawImagePixelDataInWorker(rawTileDataGroup, plotState.getColorTableId(), threeColor, .5, 1, rootUrl); + await populateRawImagePixelDataInWorker(rawTileDataGroup, plotState.getColorTableId(), threeColor, false, '', .5, 1, rootUrl); entry.rawTileDataGroup= localRawTileDataGroup; diff --git a/src/firefly/js/visualize/rawData/RawImageTilesGPU.js b/src/firefly/js/visualize/rawData/RawImageTilesGPU.js index ebdd56e39a..817cda63cb 100644 --- a/src/firefly/js/visualize/rawData/RawImageTilesGPU.js +++ b/src/firefly/js/visualize/rawData/RawImageTilesGPU.js @@ -1,13 +1,13 @@ // import {GPU} from 'gpu.js'; -import {getGlobalObj} from '../../util/WebUtil.js'; +import {createCanvas, getGlobalObj} from '../../util/WebUtil.js'; import {isArrayBuffer, once, isArray} from 'lodash'; import {TILE_SIZE} from './RawDataCommon.js'; +import {toRGB} from 'firefly/util/Color.js'; export const getGPUOps= once((GPU) => { const gpu= new GPU({mode:'gpu'}); - const standardFunc= Function('pixelAry', 'colorModel','height', 'contrast', 'offsetShift', ` const pixel= pixelAry[height-this.thread.y-1][this.thread.x]; if (pixel!==255) { @@ -83,8 +83,8 @@ export const getGPUOps= once((GPU) => { const threeCRawDataTileGPU = gpu.createKernel(threeCFunc,{graphical:true, dynamicOutput:true, dynamicArguments:true, tactic: 'speed'}); - async function createTransitionalTileWithGPU(inData, colorModel, isThreeColor, bias=.5, contrast=1, bandUse) { - const canvas= createTileWithGPU(inData,colorModel,isThreeColor, bias,contrast, bandUse); + async function createTransitionalTileWithGPU(inData, colorModel, isThreeColor, mask, maskColor, bias=.5, contrast=1, bandUse) { + const canvas= createTileWithGPU(inData,colorModel,isThreeColor, mask, maskColor, bias,contrast, bandUse); const g= getGlobalObj(); if (!g.document && g.createImageBitmap) { @@ -103,17 +103,19 @@ export const getGPUOps= once((GPU) => { * @param {RawTileData} inData * @param colorModel * @param isThreeColor + * @param {boolean} mask + * @param {string} maskColor * @param bias * @param contrast * @param bandUse * @return {HTMLCanvasElement|OffscreenCanvas} */ - function createTileWithGPU(inData, colorModel, isThreeColor, bias=.5, contrast=1, bandUse) { + function createTileWithGPU(inData, colorModel, isThreeColor, mask= false, maskColor='', bias=.5, contrast=1, bandUse) { const {width,height, pixelData3C, pixelDataStandard}= inData; - return isThreeColor ? - createRawDataTile3CRGBDataGPU(pixelData3C.map( (a) => a && get8BitAry(a)), width,height,bias,contrast,bandUse) : - createRawDataTileImageRGBDataGPU(colorModel, get8BitAry(pixelDataStandard), width,height,bias,contrast); + if (isThreeColor) return createRawDataTile3CRGBDataGPU(pixelData3C.map( (a) => a && get8BitAry(a)), width,height,bias,contrast,bandUse); + if (mask) return createRawDataTileImage(maskColor, get8BitAry(pixelDataStandard), width,height); + return createRawDataTileImageRGBDataGPU(colorModel, get8BitAry(pixelDataStandard), width,height,bias,contrast); } function createRawDataTileImageRGBDataGPU(colorModel, pixelData, width,height,bias=.5, contrast=1) { @@ -131,6 +133,40 @@ export const getGPUOps= once((GPU) => { return makeRetData(standRawDataTileGPU.canvas, width, height); } + /** + * Create a mask canvas tile + * @param {String} maskColor + * @param pixelData - each byte in pixelData represents 8 pixels, so the data is compressed into bits + * @param {number} width + * @param {number} height + * @return {HTMLCanvasElement} + */ + function createRawDataTileImage(maskColor, pixelData, width,height) { + const [red,green,blue]= toRGB(maskColor); + const imData= new ImageData(width,height); + const data= imData.data; + const len= data.length; + let pixBit; + let pixDataIdx; + for(let i= 0; i p.plotImageId===plotImageId ? {...p,rawData}: p); - const plotViewAry= replacePlotView(state.plotViewAry,{...pv,plots}); + let updatedPv; + + if (imageOverlayId) { + const overlayPlotViews= pv.overlayPlotViews?.map( (oPv) => { + if (oPv?.plot?.plotImageId!==plotImageId) return oPv; + oPv.plot= {...oPv.plot,rawData}; + return oPv; + }); + updatedPv= {...pv, overlayPlotViews}; + } + else { + const plots= pv.plots.map( (p) => p.plotImageId===plotImageId ? {...p,rawData}: p); + updatedPv= {...pv,plots}; + } + + const plotViewAry= replacePlotView(state.plotViewAry,updatedPv); return {...state, plotViewAry}; } function requestLocalData(state, action) { - const {plotImageId, plotId, dataRequested=true}= action.payload; + const {plotImageId, plotId, dataRequested=true, imageOverlayId}= action.payload; const pv= getPlotViewById(state,plotId); if (!pv) return state; - const plots= pv.plots.map( (p) => p.plotImageId===plotImageId ? {...p,dataRequested}: p); - const plotViewAry= replacePlotView(state.plotViewAry,{...pv,plots}); + let updatedPv; + if (imageOverlayId) { + const overlayPlotViews= pv.overlayPlotViews.map( (oPv) => { + if (oPv?.plot?.plotImageId!==plotImageId) return oPv; + oPv.plot= {...oPv.plot,dataRequested}; + return oPv; + }); + updatedPv= {...pv, overlayPlotViews}; + } + else { + const plots= pv.plots.map( (p) => p.plotImageId===plotImageId ? {...p,dataRequested}: p); + updatedPv= {...pv,plots}; + } + + + + const plotViewAry= replacePlotView(state.plotViewAry,updatedPv); return {...state, plotViewAry}; } diff --git a/src/firefly/js/visualize/task/ImageOverlayTask.js b/src/firefly/js/visualize/task/ImageOverlayTask.js index 5d4df3e87c..93a8fdfda1 100644 --- a/src/firefly/js/visualize/task/ImageOverlayTask.js +++ b/src/firefly/js/visualize/task/ImageOverlayTask.js @@ -1,11 +1,15 @@ /* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {get} from 'lodash'; import {logger} from '../../util/Logger.js'; import ImagePlotCntlr, {makeUniqueRequestKey, IMAGE_PLOT_KEY, dispatchPlotMask, dispatchZoom, dispatchPlotMaskLazyLoad} from '../ImagePlotCntlr.js'; -import {UserZoomTypes} from '../ZoomUtil.js'; -import {primePlot, getOverlayByPvAndId, getPlotViewById, getOverlayById} from '../PlotViewUtil.js'; +import { + primePlot, + getOverlayByPvAndId, + getPlotViewById, + getOverlayById, + hasLocalStretchByteData, canLoadStretchDataDirect +} from '../PlotViewUtil.js'; import {PlotState} from '../PlotState.js'; import {RequestType} from '../RequestType.js'; import {ZoomType} from '../ZoomType.js'; @@ -15,6 +19,7 @@ import {callGetWebPlot} from '../../rpc/PlotServicesJson.js'; import {populateFromHeader} from './PlotImageTask'; import {dispatchAddActionWatcher} from '../../core/MasterSaga'; import {isPlotIdInPvNewPlotInfoAry} from '../PlotViewUtil'; +import {changeLocalMaskColor} from 'firefly/visualize/rawData/RawDataOps.js'; const colorList= [ '#FF0000','#00FF00', '#0000FF', '#91D33D', @@ -133,33 +138,25 @@ function maskCall(vr, dispatcher, payload) { export function overlayPlotChangeAttributeActionCreator(rawAction) { return (dispatcher,getStore) => { - dispatcher(rawAction); - if (rawAction.payload.doReplot) { - const {plotId,imageOverlayId}= rawAction.payload; - var vr= getStore()[IMAGE_PLOT_KEY]; - var opv= getOverlayByPvAndId(vr,plotId, imageOverlayId); - if (!opv) return; - const {imageNumber,color, maskValue, title}= opv; - var fileKey; - if (opv.plot) { - const wpr= opv.plot.plotState.getWebPlotRequest(); - if (wpr.getRequestType()===RequestType.FILE || wpr.getFileName()) { - fileKey= wpr.getFileName(); + + let dispatchHandled= false; + const {plotId,imageOverlayId}= rawAction.payload; + if (rawAction.payload.attributes?.colorAttributes?.color && imageOverlayId) { + const vr= getStore()[IMAGE_PLOT_KEY]; + const opv= getOverlayByPvAndId(vr,plotId, imageOverlayId); + if (opv) { + const {color}= rawAction.payload.attributes.colorAttributes; + if (opv.colorAttributes.color!==color && hasLocalStretchByteData(opv.plot)) { + dispatchHandled= true; + changeLocalMaskColor(opv.plot,color) + .then( () => dispatcher(rawAction) ); } } - dispatchPlotMask({plotId,imageOverlayId, fileKey, maskValue, imageNumber, color, title}); - - vr= getStore()[IMAGE_PLOT_KEY]; - opv= getOverlayByPvAndId(vr,plotId, imageOverlayId); - const plot= primePlot(); - if (plot && get(opv, 'plot.zoomFactor') !== plot.zoomFactor) { - dispatchZoom({ - plotId:plot.plotId, - UserZoomType:UserZoomTypes.LEVEL, - level: plot.zoomFactor - }); - } } + + !dispatchHandled && dispatcher(rawAction); + + // note - rawAction.payload.doReplot - is deprecated }; } @@ -218,6 +215,7 @@ function processMaskSuccessResponse(dispatcher, payload, result) { // const imageOverlayId= WebPlotRequest.parse(result.PlotCreateHeader.plotRequestSerialize).getPlotId(); var plot= WebPlot.makeWebPlotData(imageOverlayId, PlotCreate[0], {}, true); + if (canLoadStretchDataDirect(plot,true)) plot.tileData = undefined; const resultPayload= clone(payload, {plot}); dispatcher({type: ImagePlotCntlr.PLOT_MASK, payload: resultPayload}); } diff --git a/src/firefly/js/visualize/task/PlotChangeTask.js b/src/firefly/js/visualize/task/PlotChangeTask.js index e94b5b3780..42f2938393 100644 --- a/src/firefly/js/visualize/task/PlotChangeTask.js +++ b/src/firefly/js/visualize/task/PlotChangeTask.js @@ -10,7 +10,13 @@ import { getPlotViewById, operateOnOthersInOverlayColorGroup, getPlotStateAry, - isThreeColor, findPlot, canLoadStretchData, hasClearedByteData, hasLocalStretchByteData, hasLocalRawData + isThreeColor, + findPlot, + canLoadStretchData, + hasClearedByteData, + hasLocalStretchByteData, + hasLocalRawData, + getOverlayById } from '../PlotViewUtil.js'; import {callCrop, callChangeColor, callRecomputeStretch} from '../../rpc/PlotServicesJson.js'; import {WebPlotResult} from '../WebPlotResult.js'; @@ -34,9 +40,9 @@ import {Band} from '../Band.js'; export function requestLocalDataActionCreator(rawAction) { return (dispatcher,getState) => { - const {plotId, plotImageId, dataRequested}=rawAction.payload; + const {plotId, plotImageId, dataRequested= true, imageOverlayId}=rawAction.payload; const pv= getPlotViewById(visRoot(),plotId); - const plot= findPlot(pv,plotImageId); + const plot= imageOverlayId ? getOverlayById(pv,imageOverlayId)?.plot : findPlot(pv,plotImageId); if (!canLoadStretchData(plot)) return; if (dataRequested===false) { dispatcher(rawAction); @@ -45,8 +51,7 @@ export function requestLocalDataActionCreator(rawAction) { if (plot.dataRequested) return; dispatcher(rawAction); // loadRawData(plot,dispatcher); - // loadRawData(plot,dispatcher); - loadStretchData(plot,dispatcher); + loadStretchData(pv, plot,dispatcher); }; } diff --git a/src/firefly/js/visualize/ui/ColorTableDropDownView.jsx b/src/firefly/js/visualize/ui/ColorTableDropDownView.jsx index d419a6610a..94bbaf75d5 100644 --- a/src/firefly/js/visualize/ui/ColorTableDropDownView.jsx +++ b/src/firefly/js/visualize/ui/ColorTableDropDownView.jsx @@ -158,7 +158,7 @@ const AdvancedColorPanel= ({allowPopout}) => { const [useBlue,setUseBlue]= useState( () => plot?.rawData.useBlue); const threeColor= isThreeColor(plot); - const biasInt= isArray(bias) ? bias.map( (b) => Math.trunc(b*100)) : Math.trunc(bias*40); + const biasInt= isArray(bias) ? bias.map( (b) => Math.trunc(b*40)) : Math.trunc(bias*40); const contrastInt= isArray(contrast) ? contrast.map( (c) => Math.trunc(c*10)) : Math.trunc(contrast*10); const image= isImage(plot); const plotId= plot?.plotId; diff --git a/src/firefly/js/visualize/ui/DrawLayerPanelView.jsx b/src/firefly/js/visualize/ui/DrawLayerPanelView.jsx index fe291e7fe2..ed8922984a 100644 --- a/src/firefly/js/visualize/ui/DrawLayerPanelView.jsx +++ b/src/firefly/js/visualize/ui/DrawLayerPanelView.jsx @@ -257,7 +257,7 @@ function modifyMaskColor(opv) { attributes:{colorAttributes,opacity:a}}); }); - }); + }, '', '', .58); } function modifyShape(dl, plotId) {