diff --git a/CHANGELOG.md b/CHANGELOG.md index bb795910d18..1b30438ce55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). - ### Fixed -- +- Data for disabled or invisible layers will no longer be downloaded, saving bandwidth and speeding up webKnossos in general. [#4202](https://github.com/scalableminds/webknossos/pull/4202) ### Removed - diff --git a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js index 28077cea768..f85dedde6a6 100644 --- a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -10,9 +10,14 @@ import type { APISegmentationLayer, ElementClass, } from "admin/api_flow_types"; -import type { Settings, DataLayerType } from "oxalis/store"; +import type { Settings, DataLayerType, DatasetConfiguration } from "oxalis/store"; import ErrorHandling from "libs/error_handling"; -import constants, { ViewModeValues, type Vector3, Vector3Indicies } from "oxalis/constants"; +import constants, { + ViewModeValues, + type Vector3, + Vector3Indicies, + type ViewMode, +} from "oxalis/constants"; import { aggregateBoundingBox } from "libs/utils"; import { formatExtentWithLength, formatNumberToLength } from "libs/format_utils"; import messages from "messages"; @@ -310,4 +315,20 @@ export function getResolutionByMax(dataset: APIDataset, maxDim: number): Vector3 return keyedResolutionsByMax[maxDim]; } +export function isLayerVisible( + dataset: APIDataset, + layerName: string, + datasetConfiguration: DatasetConfiguration, + viewMode: ViewMode, +): boolean { + const isPlaneMode = constants.MODES_PLANE.includes(viewMode); + if (isSegmentationLayer(dataset, layerName)) { + // Segmentation layers are only displayed in plane mode for now + return datasetConfiguration.segmentationOpacity > 0 && isPlaneMode; + } else { + const layerConfig = datasetConfiguration.layers[layerName]; + return !layerConfig.isDisabled && layerConfig.alpha > 0; + } +} + export default {}; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js b/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js index 8b9c04d7566..15aba4c03f1 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js @@ -18,6 +18,7 @@ import { getResolutions, getByteCount, getElementClass, + isLayerVisible, } from "oxalis/model/accessors/dataset_accessor"; import AsyncBucketPickerWorker from "oxalis/workers/async_bucket_picker.worker"; import type DataCube from "oxalis/model/bucket_data_handling/data_cube"; @@ -101,7 +102,7 @@ export default class LayerRenderingManager { lastAreas: OrthoViewMap; lastZoomedMatrix: M4x4; lastViewMode: ViewMode; - lastIsInvisible: boolean; + lastIsVisible: boolean; textureBucketManager: TextureBucketManager; textureWidth: number; cube: DataCube; @@ -110,7 +111,6 @@ export default class LayerRenderingManager { cachedAnchorPoint: Vector4 = [0, 0, 0, 0]; name: string; - isSegmentation: boolean; needsRefresh: boolean = false; currentBucketPickerTick: number = 0; latestTaskExecutor: LatestTaskExecutor = new LatestTaskExecutor(); @@ -121,14 +121,12 @@ export default class LayerRenderingManager { cube: DataCube, textureWidth: number, dataTextureCount: number, - isSegmentation: boolean, ) { this.name = name; this.pullQueue = pullQueue; this.cube = cube; this.textureWidth = textureWidth; this.dataTextureCount = dataTextureCount; - this.isSegmentation = isSegmentation; } refresh() { @@ -180,8 +178,7 @@ export default class LayerRenderingManager { const { viewMode } = state.temporaryConfiguration; const isArbitrary = constants.MODES_ARBITRARY.includes(viewMode); const { sphericalCapRadius } = state.userConfiguration; - const isInvisible = - this.isSegmentation && (datasetConfiguration.segmentationOpacity === 0 || isArbitrary); + const isVisible = isLayerVisible(dataset, this.name, datasetConfiguration, viewMode); if ( isAnchorPointNew || !_.isEqual(areas, this.lastAreas) || @@ -189,7 +186,7 @@ export default class LayerRenderingManager { (isArbitrary && !_.isEqual(this.lastZoomedMatrix, matrix)) || viewMode !== this.lastViewMode || sphericalCapRadius !== this.lastSphericalCapRadius || - isInvisible !== this.lastIsInvisible || + isVisible !== this.lastIsVisible || this.needsRefresh ) { this.lastSubBucketLocality = subBucketLocality; @@ -197,14 +194,14 @@ export default class LayerRenderingManager { this.lastZoomedMatrix = matrix; this.lastViewMode = viewMode; this.lastSphericalCapRadius = sphericalCapRadius; - this.lastIsInvisible = isInvisible; + this.lastIsVisible = isVisible; this.needsRefresh = false; this.currentBucketPickerTick++; this.pullQueue.clear(); let pickingPromise: Promise = Promise.resolve(dummyBuffer); - if (!isInvisible) { + if (isVisible) { const { initializedGpuFactor } = state.temporaryConfiguration.gpuSetup; pickingPromise = this.latestTaskExecutor.schedule(() => asyncBucketPick( diff --git a/frontend/javascripts/oxalis/model/data_layer.js b/frontend/javascripts/oxalis/model/data_layer.js index ff3cc3526c5..a305d4826c9 100644 --- a/frontend/javascripts/oxalis/model/data_layer.js +++ b/frontend/javascripts/oxalis/model/data_layer.js @@ -64,7 +64,6 @@ class DataLayer { this.cube, textureWidth, dataTextureCount, - isSegmentation, ); } diff --git a/frontend/javascripts/oxalis/model/sagas/prefetch_saga.js b/frontend/javascripts/oxalis/model/sagas/prefetch_saga.js index fced055a7da..bca378e2f60 100644 --- a/frontend/javascripts/oxalis/model/sagas/prefetch_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/prefetch_saga.js @@ -1,6 +1,6 @@ // @flow import { FlycamActions } from "oxalis/model/actions/flycam_actions"; -import type { OxalisState, DataLayerType } from "oxalis/store"; +import type { OxalisState } from "oxalis/store"; import { PrefetchStrategyArbitrary } from "oxalis/model/bucket_data_handling/prefetch_strategy_arbitrary"; import { PrefetchStrategySkeleton, @@ -13,14 +13,10 @@ import { getRequestLogZoomStep, getAreasFromState, } from "oxalis/model/accessors/flycam_accessor"; -import { - isSegmentationLayer, - getResolutions, - getColorLayers, -} from "oxalis/model/accessors/dataset_accessor"; +import { getResolutions, isLayerVisible } from "oxalis/model/accessors/dataset_accessor"; import DataLayer from "oxalis/model/data_layer"; import Model from "oxalis/model"; -import constants, { type Vector3 } from "oxalis/constants"; +import constants, { type Vector3, type ViewMode } from "oxalis/constants"; const PREFETCH_THROTTLE_TIME = 50; const DIRECTION_VECTOR_SMOOTHER = 0.125; @@ -42,32 +38,26 @@ export function* watchDataRelevantChanges(): Saga { ); } -function* shouldPrefetchForDataLayer(dataLayer: DataLayer): Saga { - const segmentationOpacity = yield* select( - state => state.datasetConfiguration.segmentationOpacity, +function* shouldPrefetchForDataLayer(dataLayer: DataLayer, viewMode: ViewMode): Saga { + // There is no need to prefetch data for layers that are not visible + return yield* select(state => + isLayerVisible(state.dataset, dataLayer.name, state.datasetConfiguration, viewMode), ); - const isSegmentationVisible = segmentationOpacity !== 0; - const isSegmentation = yield* select(state => isSegmentationLayer(state.dataset, dataLayer.name)); - // There is no need to prefetch data for segmentation layers that are not visible - return !isSegmentation || isSegmentationVisible; } export function* triggerDataPrefetching(previousProperties: Object): Saga { - const currentViewMode = yield* select(state => state.temporaryConfiguration.viewMode); - const isPlaneMode = constants.MODES_PLANE.includes(currentViewMode); + const viewMode = yield* select(state => state.temporaryConfiguration.viewMode); + const isPlaneMode = constants.MODES_PLANE.includes(viewMode); - if (isPlaneMode) { - const dataLayers = yield* call([Model, Model.getAllLayers]); - for (const dataLayer of dataLayers) { - if (yield* call(shouldPrefetchForDataLayer, dataLayer)) { + const dataLayers = yield* call([Model, Model.getAllLayers]); + for (const dataLayer of dataLayers) { + if (yield* call(shouldPrefetchForDataLayer, dataLayer, viewMode)) { + if (isPlaneMode) { yield* call(prefetchForPlaneMode, dataLayer, previousProperties); + } else { + yield* call(prefetchForArbitraryMode, dataLayer, previousProperties); } } - } else { - const dataLayers = yield* select(state => getColorLayers(state.dataset)); - for (const colorLayer of dataLayers) { - yield* call(prefetchForArbitraryMode, colorLayer, previousProperties); - } } } @@ -154,7 +144,7 @@ export function* prefetchForPlaneMode(layer: DataLayer, previousProperties: Obje } export function* prefetchForArbitraryMode( - layer: DataLayerType, + layer: DataLayer, previousProperties: Object, ): Saga { const position = yield* select(state => getPosition(state.flycam)); diff --git a/frontend/javascripts/test/sagas/prefetch_saga.spec.js b/frontend/javascripts/test/sagas/prefetch_saga.spec.js index 9eb70255163..ba8918cdc8f 100644 --- a/frontend/javascripts/test/sagas/prefetch_saga.spec.js +++ b/frontend/javascripts/test/sagas/prefetch_saga.spec.js @@ -24,51 +24,47 @@ const { test("Prefetch saga should trigger prefetching for the correct view mode", t => { // These are not the correct layers (those should be data_layer instances) // but they are sufficient to test this saga - const colorLayers = DATASET.dataSource.dataLayers.filter(layer => layer.category === "color"); + const allLayers = DATASET.dataSource.dataLayers; const previousProperties = {}; const saga = triggerDataPrefetching(previousProperties); saga.next(); // select viewMode - saga.next(constants.MODE_ARBITRARY); + saga.next(constants.MODE_ARBITRARY); // Model.getAllLayers + saga.next(allLayers); // shouldPrefetchForDataLayer expectValueDeepEqual( t, - saga.next(colorLayers), - call(prefetchForArbitraryMode, colorLayers[0], previousProperties), + saga.next(true), + call(prefetchForArbitraryMode, allLayers[0], previousProperties), ); - t.is(saga.next().done, true); + // Saga should not be over as there are multiple layers + t.is(saga.next().done, false); }); test("Prefetch saga should not prefetch segmentation if it is not visible", t => { // These are not the correct layers (those should be data_layer instances) // but they are sufficient to test this saga const allLayers = DATASET.dataSource.dataLayers; - const colorLayers = DATASET.dataSource.dataLayers.filter(layer => layer.category === "color"); const previousProperties = {}; + const viewMode = constants.MODE_PLANE_TRACING; const saga = triggerDataPrefetching(previousProperties); saga.next(); // select viewMode - expectValueDeepEqual( - t, - saga.next(constants.MODE_PLANE_TRACING), - call([Model, Model.getAllLayers]), - ); + expectValueDeepEqual(t, saga.next(viewMode), call([Model, Model.getAllLayers])); - let shouldPrefetchSaga = execCall(t, saga.next(allLayers)); // shouldPrefetchForDataLayer - shouldPrefetchSaga.next(); - shouldPrefetchSaga.next(0); // select segmentationOpacity - const shouldPrefetchColorLayer = shouldPrefetchSaga.next(false).value; - // The color layer should be prefetched, although the segmentationOpacity is 0 - t.is(shouldPrefetchColorLayer, true); + let shouldPrefetchSaga = execCall(t, saga.next(allLayers, viewMode)); // shouldPrefetchForDataLayer + shouldPrefetchSaga.next(); // isLayerVisible + const shouldPrefetchDataLayer = shouldPrefetchSaga.next(true).value; + // The color layer should be prefetched, because it is visible + t.is(shouldPrefetchDataLayer, true); expectValueDeepEqual( t, - saga.next(shouldPrefetchColorLayer), - call(prefetchForPlaneMode, colorLayers[0], previousProperties), + saga.next(shouldPrefetchDataLayer), + call(prefetchForPlaneMode, allLayers[0], previousProperties), ); shouldPrefetchSaga = execCall(t, saga.next()); // shouldPrefetchForDataLayer - shouldPrefetchSaga.next(); - shouldPrefetchSaga.next(0); // select segmentationOpacity - const shouldPrefetchSegmentationLayer = shouldPrefetchSaga.next(true).value; + shouldPrefetchSaga.next(); // isLayerVisible + const shouldPrefetchSegmentationLayer = shouldPrefetchSaga.next(false).value; // The segmentation layer should not be prefetched because it is not visible t.is(shouldPrefetchSegmentationLayer, false); - // So the saga should be over afterwards, no prefetch call + // The saga should be over afterwards - no prefetch call t.is(saga.next(shouldPrefetchSegmentationLayer).done, true); });