Skip to content

Commit

Permalink
Merge branch 'master' of github.com:scalableminds/webknossos into doc…
Browse files Browse the repository at this point in the history
…s_lili

* 'master' of github.com:scalableminds/webknossos:
  Add loaded meshes to sharing link (#5993)
  • Loading branch information
hotzenklotz committed Feb 21, 2022
2 parents dca6181 + 2854aab commit 1d25377
Show file tree
Hide file tree
Showing 50 changed files with 1,048 additions and 603 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ jobs:
nightly:
docker:
- image: scalableminds/puppeteer:master
resource_class: large
steps:
- checkout
- run:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added experimental min-cut feature to split a segment in a volume tracing with two seeds. [#5885](https://github.com/scalableminds/webknossos/pull/5885)
- Annotations with multiple volume layers can now be uploaded. (Note that merging multiple annotations with multiple volume layers each is not supported.) [#6028](https://github.com/scalableminds/webknossos/pull/6028)
- Decrease volume annotation download latency by using a different compression level. [#6036](https://github.com/scalableminds/webknossos/pull/6036)
- The visible meshes are now included in the link copied from the "Share" modal or the "Share" button next to the dataset position. They are automatically loaded for users that open the shared link. [#5993](https://github.com/scalableminds/webknossos/pull/5993)

### Changed
- Upgraded webpack build tool to v5 and all other webpack related dependencies to their latest version. Enabled persistent caching which speeds up server restarts during development as well as production builds. [#5969](https://github.com/scalableminds/webknossos/pull/5969)
Expand Down
76 changes: 50 additions & 26 deletions docs/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,33 +130,57 @@ To get the sharing link of an annotation, follow the same steps as for changing

#### Sharing Link Format

As already indicated, the sharing link encodes certain properties, like the current position, rotation, zoom, and active mapping to ensure that users you share the link with see the same things you saw when you copied the link. Alternatively, the link can be crafted manually or programmatically to direct users to specific locations in a dataset. The information is json encoded in the URL fragment and has the following format (flow type definition):

```javascript
type MappingType = "JSON" | "HDF5";
type ViewMode = "orthogonal" | "oblique" | "flight" | "volume";
type Vector3 = [number, number, number];

type UrlStateByLayer = {
[layerName: string]: {
mappingInfo?: {
mappingName: string,
mappingType: MappingType,
agglomerateIdsToImport?: [number],
As already indicated, the sharing link encodes certain properties, like the current position, rotation, zoom, active mapping, and visible meshes to ensure that users you share the link with see the same things you saw when you copied the link. Alternatively, the link can be crafted manually or programmatically to direct users to specific locations in a dataset. The information is json encoded in the URL fragment and has the following format (flow type definition):

<details>
<summary>URL Fragment Format</summary>
```javascript
type MappingType = "JSON" | "HDF5";
type ViewMode = "orthogonal" | "oblique" | "flight" | "volume";
type Vector3 = [number, number, number];

type BaseMeshUrlDescriptor = {|
+segmentId: number,
+seedPosition: Vector3,
|};
type AdHocMeshUrlDescriptor = {|
...BaseMeshUrlDescriptor,
+isPrecomputed: false,
mappingName: ?string,
mappingType: ?MappingType,
|};
type PrecomputedMeshUrlDescriptor = {|
...BaseMeshUrlDescriptor,
+isPrecomputed: true,
meshFileName: string,
|};
type MeshUrlDescriptor = AdHocMeshUrlDescriptor | PrecomputedMeshUrlDescriptor;

type UrlStateByLayer = {
[layerName: string]: {
meshInfo?: {
meshFileName: ?string,
meshes: Array<MeshUrlDescriptor>,
},
mappingInfo?: {
mappingName: string,
mappingType: MappingType,
agglomerateIdsToImport?: Array<number>,
},
},
},
};

type UrlManagerState = {|
position?: Vector3,
mode?: ViewMode,
zoomStep?: number,
activeNode?: number,
rotation?: Vector3,
stateByLayer?: UrlStateByLayer,
|};

```
};

type UrlManagerState = {|
position?: Vector3,
mode?: ViewMode,
zoomStep?: number,
activeNode?: number,
rotation?: Vector3,
stateByLayer?: UrlStateByLayer,
|};

```
</details>
To avoid having to create annotations in advance when programmatically crafting links, a sandbox tracing can be used. A sandbox tracing is always accessible through the same URL and offers all available tracing features, however, changes are not saved. At any point, users can decide to copy the current state to their account. The sandbox can be accessed at `<webknossos_host>/datasets/<organization>/<dataset>/sandbox/skeleton`.
Expand Down
30 changes: 18 additions & 12 deletions frontend/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ import {
type TracingType,
type WkConnectDatasetConfig,
} from "types/api_flow_types";
import { ControlModeEnum, type Vector3, type Vector6, MappingStatusEnum } from "oxalis/constants";
import { ControlModeEnum, type Vector3, type Vector6 } from "oxalis/constants";
import type {
DatasetConfiguration,
Tracing,
TraceOrViewCommand,
AnnotationType,
ActiveMappingInfo,
MappingType,
VolumeTracing,
} from "oxalis/store";
import type { NewTask, TaskCreationResponseContainer } from "admin/task/task_create_bulk_view";
Expand Down Expand Up @@ -1769,25 +1769,31 @@ export function getMeshData(id: string): Promise<ArrayBuffer> {

// These parameters are bundled into an object to avoid that the computeIsosurface function
// receives too many parameters, since this doesn't play well with the saga typings.
type IsosurfaceRequest = {
type IsosurfaceRequest = {|
position: Vector3,
zoomStep: number,
segmentId: number,
subsamplingStrides: Vector3,
cubeSize: Vector3,
scale: Vector3,
};
mappingName: ?string,
mappingType: ?MappingType,
|};

export function computeIsosurface(
requestUrl: string,
mappingInfo: ActiveMappingInfo,
isosurfaceRequest: IsosurfaceRequest,
): Promise<{ buffer: ArrayBuffer, neighbors: Array<number> }> {
const { position, zoomStep, segmentId, subsamplingStrides, cubeSize, scale } = isosurfaceRequest;
const mapping =
mappingInfo.mappingStatus !== MappingStatusEnum.DISABLED ? mappingInfo.mappingName : undefined;
const mappingType =
mappingInfo.mappingStatus !== MappingStatusEnum.DISABLED ? mappingInfo.mappingType : undefined;
const {
position,
zoomStep,
segmentId,
subsamplingStrides,
cubeSize,
scale,
mappingName,
mappingType,
} = isosurfaceRequest;
return doWithToken(async token => {
const { buffer, headers } = await Request.sendJSONReceiveArraybufferWithHeaders(
`${requestUrl}/isosurface?token=${token}`,
Expand All @@ -1801,8 +1807,8 @@ export function computeIsosurface(
zoomStep,
// Segment to build mesh for
segmentId,
// Name of mapping to apply before building mesh (optional)
mapping,
// Name and type of mapping to apply before building mesh (optional)
mapping: mappingName,
mappingType,
// "size" of each voxel (i.e., only every nth voxel is considered in each dimension)
subsamplingStrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import update from "immutability-helper";
import type { APIUser, APITeam, APITeamMembership } from "types/api_flow_types";
import { updateUser, getEditableTeams } from "admin/admin_rest_api";
import messages from "messages";
import * as Utils from "libs/utils";

const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
Expand Down Expand Up @@ -129,7 +130,7 @@ class PermissionsAndTeamsModalView extends React.PureComponent<TeamRoleModalProp
setPermissionsAndTeams = () => {
const newUserPromises = this.props.users.map(user => {
if (this.props.selectedUserIds.includes(user.id)) {
const newTeams = ((Object.values(this.state.selectedTeams): any): Array<APITeamMembership>);
const newTeams = Utils.values(this.state.selectedTeams);
const newUser = Object.assign({}, user, { teams: newTeams });
if (this.props.activeUser.isAdmin && this.props.selectedUserIds.length === 1) {
// If the current user is admin and only one user is edited we also update the permissions.
Expand Down
67 changes: 41 additions & 26 deletions frontend/javascripts/libs/task_pool.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,51 @@
// @flow
import { type Saga, type Task, join, call, fork } from "oxalis/model/sagas/effect-generators";

export default function processTaskWithPool<T>(
tasks: Array<() => Promise<T>>,
export default function* processTaskWithPool(
tasks: Array<() => Saga<void>>,
poolSize: number,
): Promise<Array<T>> {
return new Promise((resolve, reject) => {
const promises = [];
let isFinalResolveScheduled = false;
): Saga<void> {
const startedTasks: Array<Task<void>> = [];
let isFinalResolveScheduled = false;
let error = null;

const startNextTask = () => {
if (tasks.length === 0) {
if (!isFinalResolveScheduled) {
isFinalResolveScheduled = true;
function* forkSafely(fn): Saga<void> {
// Errors from forked tasks cannot be caught, see https://redux-saga.js.org/docs/advanced/ForkModel/#error-propagation
// However, the task pool should not abort if a single task fails.
// Therefore, use this wrapper to safely execute all tasks and possibly rethrow the last error in the end.
try {
yield* call(fn);
} catch (e) {
error = e;
}
}

function* startNextTask(): Saga<void> {
if (tasks.length === 0) {
if (!isFinalResolveScheduled) {
isFinalResolveScheduled = true;

// All tasks were kicked off, which is why all promises can be
// awaited now together.
Promise.all(promises).then(resolve, reject);
}
return;
// All tasks were kicked off, which is why all tasks can be
// awaited now together.
yield* join(startedTasks);
if (error != null) throw error;
}
return;
}

const task = tasks.shift();
const newPromise = task();
promises.push(newPromise);
const task = tasks.shift();
const newTask = yield* fork(forkSafely, task);
startedTasks.push(newTask);

// If that promise is done, process a new one (that way,
// the pool size stays constant until the queue is almost empty.)
newPromise.then(startNextTask, startNextTask);
};
// If that task is done, process a new one (that way,
// the pool size stays constant until the queue is almost empty.)
yield* join(newTask);
yield* call(startNextTask);
}

for (let i = 0; i < poolSize; i++) {
startNextTask();
}
});
for (let i = 0; i < poolSize; i++) {
yield* fork(startNextTask);
}
// The saga will wait for all forked tasks to terminate before returning, because
// fork() creates attached forks (in contrast to spawn()).
}
13 changes: 9 additions & 4 deletions frontend/javascripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export function mod(x: number, n: number) {
return ((x % n) + n) % n;
}

export function values<K, V>(o: { [K]: V }): Array<V> {
// $FlowIssue[incompatible-return] remove once https://github.com/facebook/flow/issues/2221 is fixed
return Object.values(o);
}

export function map2<A, B>(fn: (A, number) => B, tuple: [A, A]): [B, B] {
const [x, y] = tuple;
return [fn(x, 0), fn(y, 1)];
Expand Down Expand Up @@ -521,8 +526,8 @@ export function filterWithSearchQueryOR<T: { +[string]: mixed }, P: $Keys<T>>(
_.some(properties, fieldName => {
const value = typeof fieldName === "function" ? fieldName(model) : model[fieldName];
if (value != null && (typeof value === "string" || value instanceof Object)) {
const values = getRecursiveValues(value);
return _.some(values, v => v != null && v.toString().match(regexp));
const recursiveValues = getRecursiveValues(value);
return _.some(recursiveValues, v => v != null && v.toString().match(regexp));
} else {
return false;
}
Expand Down Expand Up @@ -552,8 +557,8 @@ export function filterWithSearchQueryAND<T: { +[string]: mixed }, P: $Keys<T>>(
_.some(properties, fieldName => {
const value = typeof fieldName === "function" ? fieldName(model) : model[fieldName];
if (value !== null && (typeof value === "string" || value instanceof Object)) {
const values = getRecursiveValues(value);
return _.some(values, v => v != null && v.toString().match(pattern));
const recursiveValues = getRecursiveValues(value);
return _.some(recursiveValues, v => v != null && v.toString().match(pattern));
} else {
return false;
}
Expand Down
27 changes: 14 additions & 13 deletions frontend/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ import {
} from "oxalis/model/helpers/position_converter";
import { callDeep } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers";
import { centerTDViewAction } from "oxalis/model/actions/view_mode_actions";
import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions";
import {
loadAdHocMeshAction,
loadPrecomputedMeshAction,
} from "oxalis/model/actions/segmentation_actions";
import { discardSaveQueuesAction } from "oxalis/model/actions/save_actions";
import {
doWithToken,
Expand Down Expand Up @@ -79,10 +82,7 @@ import {
getRotation,
getRequestLogZoomStep,
} from "oxalis/model/accessors/flycam_accessor";
import {
loadMeshFromFile,
maybeFetchMeshFiles,
} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper";
import { maybeFetchMeshFiles } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper";
import { overwriteAction } from "oxalis/model/helpers/overwrite_action_middleware";
import { parseNml } from "oxalis/model/helpers/nml_helpers";
import { rotate3DViewTo } from "oxalis/controller/camera_controller";
Expand Down Expand Up @@ -140,6 +140,7 @@ import * as Utils from "libs/utils";
import dimensions from "oxalis/model/dimensions";
import messages from "messages";
import window, { location } from "libs/window";
import DataLayer from "oxalis/model/data_layer";

type OutdatedDatasetConfigurationKeys = "segmentationOpacity" | "isSegmentationDisabled";

Expand Down Expand Up @@ -1009,9 +1010,7 @@ class DataApi {
*/
async reloadBuckets(layerName: string): Promise<void> {
await Promise.all(
Object.keys(this.model.dataLayers).map(async currentLayerName => {
const dataLayer = this.model.dataLayers[currentLayerName];

Utils.values(this.model.dataLayers).map(async (dataLayer: DataLayer) => {
if (dataLayer.name === layerName) {
if (dataLayer.cube.isSegmentation) {
await Model.ensureSavedState();
Expand All @@ -1030,7 +1029,7 @@ class DataApi {
if (hasVolumeTracings(Store.getState().tracing)) {
await Model.ensureSavedState();
}
_.forEach(this.model.dataLayers, dataLayer => {
Utils.values(this.model.dataLayers).forEach((dataLayer: DataLayer) => {
dataLayer.cube.collectAllBuckets();
dataLayer.layerRenderingManager.refresh();
});
Expand Down Expand Up @@ -1624,9 +1623,9 @@ class DataApi {
* const availableMeshFiles = await api.data.getAvailableMeshFiles();
* api.data.setActiveMeshFile(availableMeshFiles[0]);
*
* await api.data.loadPrecomputedMesh(segmentId, currentPosition);
* api.data.loadPrecomputedMesh(segmentId, currentPosition);
*/
async loadPrecomputedMesh(segmentId: number, seedPosition: Vector3, layerName: ?string) {
loadPrecomputedMesh(segmentId: number, seedPosition: Vector3, layerName: ?string) {
const state = Store.getState();
const effectiveLayerName = getNameOfRequestedOrVisibleSegmentationLayer(state, layerName);
if (!effectiveLayerName) {
Expand Down Expand Up @@ -1659,7 +1658,9 @@ class DataApi {
}
}

await loadMeshFromFile(segmentId, seedPosition, meshFileName, segmentationLayer, dataset);
Store.dispatch(
loadPrecomputedMeshAction(segmentId, seedPosition, meshFileName, effectiveLayerName),
);
}

/**
Expand All @@ -1671,7 +1672,7 @@ class DataApi {
* api.data.computeMeshOnDemand(segmentId, currentPosition);
*/
computeMeshOnDemand(segmentId: number, seedPosition: Vector3) {
Store.dispatch(changeActiveIsosurfaceCellAction(segmentId, seedPosition, true));
Store.dispatch(loadAdHocMeshAction(segmentId, seedPosition));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/api/cross_origin_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const onMessage = async event => {
case "loadPrecomputedMesh": {
const segmentId = args[0];
const seedPosition = args[1];
await api.data.loadPrecomputedMesh(segmentId, seedPosition);
api.data.loadPrecomputedMesh(segmentId, seedPosition);
break;
}
case "setMeshVisibility": {
Expand Down
Loading

0 comments on commit 1d25377

Please sign in to comment.