diff --git a/docs/source/channel_map.rst b/docs/source/channel_map.rst index cd426f2..6f13ca8 100644 --- a/docs/source/channel_map.rst +++ b/docs/source/channel_map.rst @@ -30,10 +30,15 @@ Channel Map User -> Frontend: Request channel map activate Frontend - Frontend -> Backend : 5. SET_IMAGE_CHANNELS (channel_map_enabled) - activate Backend - Frontend <--[#red] Backend : 6. RASTER_TILE_DATA [Check 1] (per channel) - deactivate Backend + + loop One request per channel + Frontend -> Backend : 5. SET_IMAGE_CHANNELS (channel_map_enabled) + activate Backend + Frontend <--[#red] Backend : 6. RASTER_TILE_DATA [Check 3] + Frontend <--[#red] Backend : 7. CHANNEL_MAP_FLOW_CONTROL [Check 4] + deactivate Backend + end + User <-- Frontend: Displays channel map deactivate Frontend @@ -44,6 +49,16 @@ See the `source code { const msgController = MessageController.Instance; + const rasterTileDataLen = tiles.length; // one RasterTileData per tile + const rasterTileMsgLen = rasterTileDataLen + 2; // + RasterTileSync start & end + describe(`Register a session`, () => { beforeAll(async () => { await msgController.connect(testServerUrl); @@ -302,6 +135,8 @@ describe('CHANNEL_MAP: Test loading multiple images and generating their channel describe(`Go to "${assertItem.fileList.directory}" folder`, () => { assertItem.fileOpenGroup.map((input, index) => { + const fileId = assertItem.fileOpenAckGroup[index].fileId as number; + describe(`Open ${input.file}`, () => { let OpenFileAck: any; test( @@ -358,230 +193,84 @@ describe('CHANNEL_MAP: Test loading multiple images and generating their channel }); }); - describe(`1) Set image channel map for the file "${assertItem.fileOpenGroup[index].file}"`, () => { - let RasterTileMsgTemp: any; - const rasterTileDataLen = assertItem.setImageChannelGroup[index].requiredTiles.tiles.length; - const rasterTileMsgLen = rasterTileDataLen + 2; - - test( - `Three RASTER_TILE_DATA should arrive within ${readFileTimeout} ms`, - async () => { - msgController.setChannels(assertItem.setImageChannelGroup[index]); - const maxChan = assertItem.setImageChannelGroup[index].channelRange.max; - const minChan = assertItem.setImageChannelGroup[index].channelRange.min; - const channels = maxChan - minChan + 1; - RasterTileMsgTemp = await ChannelMapStream(rasterTileDataLen, channels); - }, - readFileTimeout - ); - - // Check file Ids - test(`1st RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[1].fileId).toEqual(assertItem.rasterTileDataGroup[index].fileId); - }); - test(`2nd RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].fileId).toEqual( - assertItem.rasterTileDataGroup[index].fileId - ); - }); - test(`3rd RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].fileId).toEqual( - assertItem.rasterTileDataGroup[index].fileId - ); - }); - - // Check channels - test(`1st RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 1}`, () => { - expect(RasterTileMsgTemp[1].channel).toEqual(assertItem.rasterTileDataGroup[index].channel + 1); - }); - test(`2nd RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 2}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].channel).toEqual( - assertItem.rasterTileDataGroup[index].channel + 2 + // Render the channel map view one channel at a time. Each request is answered by + // that channel's raster tiles followed by a CHANNEL_MAP_FLOW_CONTROL ack; the next + // request is only sent after the previous channel completes. + const channelMapSequence = [...assertItem.channelMapView, assertItem.channelMapJump]; + + channelMapSequence.forEach((channel, seq) => { + const label = + seq < assertItem.channelMapView.length + ? `view channel ${channel}` + : `jump to channel ${channel}`; + + describe(`Channel map (${label}) for the file "${assertItem.fileOpenGroup[index].file}"`, () => { + let response: { rasterTileMsgs: any[]; flowControl: any }; + + test( + `RASTER_TILE_DATA and CHANNEL_MAP_FLOW_CONTROL should arrive within ${readFileTimeout} ms`, + async () => { + msgController.setChannels(channelMapRequest(fileId, channel)); + response = await ChannelMapStream(rasterTileDataLen); + // one channel = # tiles + 2 RasterTileSync + expect(response.rasterTileMsgs.length).toEqual(rasterTileMsgLen); + }, + readFileTimeout ); - }); - test(`3rd RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 3}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].channel).toEqual( - assertItem.rasterTileDataGroup[index].channel + 3 - ); - }); - - // Check stokes - test(`1st RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[1].stokes).toEqual(assertItem.rasterTileDataGroup[index].stokes); - }); - test(`2nd RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].stokes).toEqual( - assertItem.rasterTileDataGroup[index].stokes - ); - }); - test(`3rd RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].stokes).toEqual( - assertItem.rasterTileDataGroup[index].stokes - ); - }); - - // Check tiles - const idx1 = rasterTileMsgLen - 1 - 1; - const idx2 = 2 * rasterTileMsgLen - 1 - 1; - const idx3 = 3 * rasterTileMsgLen - 1 - 1; - - // Check tiles length - test(`1st RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx1].tiles.length).toEqual(1); - }); - test(`2nd RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx2].tiles.length).toEqual(1); - }); - test(`3rd RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx3].tiles.length).toEqual(1); - }); - - // Check tiles layer - test(`1st RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx1].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - test(`2nd RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx2].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - test(`3rd RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx3].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - - // Check tiles x - test(`1st RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - - // Check tiles y - test(`1st RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - - // Check tiles width - test(`1st RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - - // Check tiles height - test(`1st RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.height = ${assertItem.tileDataGroup[index].height}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - }); - - describe(`2) Set image channel map for the file "${assertItem.fileOpenGroup[index].file}"`, () => { - let RasterTileMsgTemp: any; - const rasterTileDataLen = assertItem.setImageChannelGroup2[index].requiredTiles.tiles.length; - const resterTileMsgLen = rasterTileDataLen + 2; - - test( - `A new RASTER_TILE_DATA should arrive within ${readFileTimeout} ms`, - async () => { - msgController.setChannels(assertItem.setImageChannelGroup2[index]); - const maxChan = assertItem.setImageChannelGroup2[index].channelRange.max; - const minChan = assertItem.setImageChannelGroup2[index].channelRange.min; - const channels = maxChan - minChan + 1; - RasterTileMsgTemp = await ChannelMapStream(rasterTileDataLen, channels); - }, - readFileTimeout - ); - - // Check channels - test(`New RASTER_TILE_DATA.channel = ${assertItem.setImageChannelGroup2[index].channel}`, () => { - expect(RasterTileMsgTemp[1].channel).toEqual(assertItem.setImageChannelGroup2[index].channel); - }); - - // Check stokes - test(`New RASTER_TILE_DATA.stokes = ${assertItem.setImageChannelGroup2[index].stokes}`, () => { - expect(RasterTileMsgTemp[1].stokes).toEqual(assertItem.setImageChannelGroup2[index].stokes); - }); - - // Check tiles - const idx = resterTileMsgLen - 1 - 1; - - // Check tiles length - test(`New RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx].tiles.length).toEqual(1); - }); - - // Check tiles layer - test(`New RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - - // Check tiles x - test(`New RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - - // Check tiles y - test(`New RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - - // Check tiles width - test(`New RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - // Check tiles height - test(`New RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } + // Check the flow control acknowledgment + test(`CHANNEL_MAP_FLOW_CONTROL.file_id = ${fileId}`, () => { + expect(response.flowControl.fileId).toEqual(fileId); + }); + test(`CHANNEL_MAP_FLOW_CONTROL.completed_channel = ${channel}`, () => { + expect(response.flowControl.completedChannel).toEqual(channel); + }); + test(`CHANNEL_MAP_FLOW_CONTROL.status = COMPLETED`, () => { + expect(response.flowControl.status).toEqual(CARTA.ChannelMapFlowControl.Status.COMPLETED); + }); + + // Check channel / stokes of the raster tiles (first RasterTileData follows the sync start) + test(`RASTER_TILE_DATA.file_id = ${fileId}`, () => { + expect(response.rasterTileMsgs[1].fileId).toEqual(fileId); + }); + test(`RASTER_TILE_DATA.channel = ${channel}`, () => { + expect(response.rasterTileMsgs[1].channel).toEqual(channel); + }); + test(`RASTER_TILE_DATA.stokes = 0`, () => { + expect(response.rasterTileMsgs[1].stokes).toEqual(0); + }); + + // Check tiles (RasterTileData occupy indices 1 .. rasterTileDataLen) + test(`each RASTER_TILE_DATA.tiles.length = 1`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles.length).toEqual(1); + } + }); + test(`each RASTER_TILE_DATA.tiles.layer = ${assertItem.tileData.layer}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles[0].layer).toEqual(assertItem.tileData.layer); + } + }); + test(`each RASTER_TILE_DATA.tiles.x in ${assertItem.tileData.x}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.x).toContain(response.rasterTileMsgs[i].tiles[0].x); + } + }); + test(`each RASTER_TILE_DATA.tiles.y in ${assertItem.tileData.y}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.y).toContain(response.rasterTileMsgs[i].tiles[0].y); + } + }); + test(`each RASTER_TILE_DATA.tiles.width in ${assertItem.tileData.width}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.width).toContain(response.rasterTileMsgs[i].tiles[0].width); + } + }); + test(`each RASTER_TILE_DATA.tiles.height = ${assertItem.tileData.height}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles[0].height).toEqual(assertItem.tileData.height); + } + }); }); }); }); diff --git a/src/test/FILEINFO_EXCEPTIONS.test.ts b/src/test/FILEINFO_EXCEPTIONS.test.ts index 57e2e50..1f15dbf 100644 --- a/src/test/FILEINFO_EXCEPTIONS.test.ts +++ b/src/test/FILEINFO_EXCEPTIONS.test.ts @@ -59,4 +59,4 @@ describe('FILEINFO_EXCEPTIONS: Testing error handle of file info generation', () }); afterAll(() => msgController.closeConnection()); }); -}); \ No newline at end of file +}); diff --git a/src/test/MessageController.ts b/src/test/MessageController.ts index b1971f6..bbd356b 100644 --- a/src/test/MessageController.ts +++ b/src/test/MessageController.ts @@ -94,6 +94,7 @@ export class MessageController { readonly fittingProgressStream: Subject; readonly vectorTileStream: Subject; readonly pvPreviewStream: Subject; + readonly channelMapFlowControlStream: Subject; private readonly decoderMap: Map; private constructor() { @@ -123,6 +124,7 @@ export class MessageController { this.fittingProgressStream = new Subject(); this.vectorTileStream = new Subject(); this.pvPreviewStream = new Subject(); + this.channelMapFlowControlStream = new Subject(); // Construct handler and decoder maps this.decoderMap = new Map([ @@ -378,6 +380,13 @@ export class MessageController { handler: this.onDeferredResponse, }, ], + [ + CARTA.EventType.CHANNEL_MAP_FLOW_CONTROL, + { + messageClass: CARTA.ChannelMapFlowControl, + handler: this.onStreamedChannelMapFlowControl, + }, + ], ]); // check ping every 5 seconds @@ -412,7 +421,9 @@ export class MessageController { if (wasNeverActive) { const def = this.deferredMap.get(requestId); if (def) { - def.reject(new Error(`WebSocket closed before connect: code=${ev.code} reason=${ev.reason}`)); + def.reject( + new Error(`WebSocket closed before connect: code=${ev.code} reason=${ev.reason}`) + ); this.deferredMap.delete(requestId); } } @@ -848,16 +859,12 @@ export class MessageController { let stokes = input.stokes; let requiredTiles = input.requiredTiles; let channelMapEnabled = input.channelMapEnabled; - let channelRange = input.channelRange; - let currentRange = input.currentRange; if (this.connectionStatus === ConnectionStatus.ACTIVE) { const message = CARTA.SetImageChannels.create({ fileId: fileId, channel: channel, stokes: stokes, requiredTiles: requiredTiles, - channelRange: channelRange, - currentRange: currentRange, channelMapEnabled: channelMapEnabled, }); this.logEvent(CARTA.EventType.SET_IMAGE_CHANNELS, this.eventCounter, message, false); @@ -1030,14 +1037,12 @@ export class MessageController { let tiles = input.tiles; let type = input.compressionType; let quality = input.compressionQuality; - let currentTiles = input.currentTiles; if (this.connectionStatus === ConnectionStatus.ACTIVE) { const message = CARTA.AddRequiredTiles.create({ fileId: fileId, tiles: tiles, compressionType: type, compressionQuality: quality, - currentTiles: currentTiles, }); this.logEvent(CARTA.EventType.ADD_REQUIRED_TILES, this.eventCounter, message, false); if (this.sendEvent(CARTA.EventType.ADD_REQUIRED_TILES, CARTA.AddRequiredTiles.encode(message).finish())) { @@ -1468,6 +1473,10 @@ export class MessageController { this.pvPreviewStream.next(previewData); } + private onStreamedChannelMapFlowControl(_eventId: number, flowControl: CARTA.ChannelMapFlowControl) { + this.channelMapFlowControlStream.next(flowControl); + } + private sendEvent(eventType: CARTA.EventType, payload: Uint8Array): boolean { if (this.connection.readyState === WebSocket.OPEN) { const eventData = new Uint8Array(8 + payload.byteLength); diff --git a/src/test/MyClient.ts b/src/test/MyClient.ts index a295366..176695b 100644 --- a/src/test/MyClient.ts +++ b/src/test/MyClient.ts @@ -155,24 +155,41 @@ function Stream(cartaType: any, InputNum?: number) { }); } -function ChannelMapStream(rasterTileDataLen: number, channels: number) { - return new Promise((resolve, reject) => { +// Collects the response to a single channel map request. Since the refactor, the backend +// processes one channel per SET_IMAGE_CHANNELS request and replies with that channel's raster +// tiles (2 RasterTileSync + rasterTileDataLen RasterTileData) followed by a single +// CHANNEL_MAP_FLOW_CONTROL message reporting the request outcome. Resolves once both the tile +// batch and the flow control message have arrived. +function ChannelMapStream(rasterTileDataLen: number) { + return new Promise<{ rasterTileMsgs: any[]; flowControl: any }>((resolve, reject) => { const msgController = MessageController.Instance; - const rasterTileMsgLen = (rasterTileDataLen + 2) * channels; // # of RasterTileData + 2 RasterTileSync per channel + const rasterTileMsgLen = rasterTileDataLen + 2; // # of RasterTileData + 2 RasterTileSync for one channel let count = 0; let rasterTileMsgs: any[] = []; - let rasterTileSyncStream = msgController.rasterSyncStream.pipe(take(2 * channels)); // 2 RasterTileSync per channel + let flowControl: any = null; + + const tryResolve = () => { + if (count === rasterTileMsgLen && flowControl !== null) { + resolve({ rasterTileMsgs, flowControl }); + } + }; + + const rasterTileSyncStream = msgController.rasterSyncStream.pipe(take(2)); // start + end rasterTileSyncStream.subscribe((data) => { count++; rasterTileMsgs.push(data); - if (data.endSync && count === rasterTileMsgLen) { - resolve(rasterTileMsgs); - } + tryResolve(); }); - let rasterTileDataStream = msgController.rasterTileStream.pipe(take(rasterTileDataLen * channels)); + const rasterTileDataStream = msgController.rasterTileStream.pipe(take(rasterTileDataLen)); rasterTileDataStream.subscribe((data) => { count++; rasterTileMsgs.push(data); + tryResolve(); + }); + const flowControlStream = msgController.channelMapFlowControlStream.pipe(take(1)); + flowControlStream.subscribe((data) => { + flowControl = data; + tryResolve(); }); }); } diff --git a/src/test/config.json b/src/test/config.json index 1902afa..1f91f91 100644 --- a/src/test/config.json +++ b/src/test/config.json @@ -1,6 +1,6 @@ { "serverURL0": "ws://127.0.0.1:3002", - "icdVersion": 30, + "icdVersion": 31, "path": { "root": ".", "base": "$BASE",