Skip to content

Commit 07fb7e8

Browse files
committed
Merge branch 'master' of github.com:scalableminds/webknossos into no_type
* 'master' of github.com:scalableminds/webknossos: fix error when loading agglomerate skeleton for single-segment agglomerate (#6294) Editable Mappings aka Supervoxel Proofreading (#6195) Increase maximum interpolation depth to 100 (#6292) Add download modal to dataset view actions (#6283) Drop "Explorational" from info tab (#6290) Allow version history view in annotations not owned by you (#6274) Bucket loading meter (#6269) Revert "Merge "Shared Annotations" with "My annotations" (#6230)" (#6286) Merge "Shared Annotations" with "My annotations" (#6230)
2 parents 6aed61a + 5156a4d commit 07fb7e8

File tree

134 files changed

+3854
-1031
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

134 files changed

+3854
-1031
lines changed

CHANGELOG.unreleased.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,25 @@ and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MIC
88
For upgrade instructions, please check the [migration guide](MIGRATIONS.released.md).
99

1010
## Unreleased
11+
1112
[Commits](https://github.com/scalableminds/webknossos/compare/22.06.1...HEAD)
1213

1314
### Added
14-
15+
- Added a image data download speed indicator to the statusbar. On hover a tooltip is shown that show the total amount of downloaded shard data. [#6269](https://github.com/scalableminds/webknossos/pull/6269)
1516
- Added a warning for when the resolution in the XY viewport on z=1-downsampled datasets becomes too low, explaining the problem and how to mitigate it. [#6255](https://github.com/scalableminds/webknossos/pull/6255)
17+
- Provide a UI to download/export a dataset in view-mode. The UI explains how to access the data with the python library. [#6283](https://github.com/scalableminds/webknossos/pull/6283)
18+
- Added the possibility to view and download older versions of read-only annotations. [#6274](https://github.com/scalableminds/webknossos/pull/6274)
19+
- Added a proofreading tool which can be used to edit agglomerate mappings. After activating an agglomerate mapping the proofreading tool can be selected. While the tool is active, agglomerates can be clicked to load their agglomerate skeletons. Use the context menu to delete or create edges for those agglomerate skeletons to split or merge agglomerates. The changes will immediately reflect in the segmentation and meshes. [#6195](https://github.com/scalableminds/webknossos/pull/6195)
1620
- Add new backend API routes for working with annotations without having to provide a 'type' argument [#6285](https://github.com/scalableminds/webknossos/pull/6285)
1721

1822
### Changed
1923

2024
- For the api routes that return annotation info objects, the user field was renamed to owner. User still exists as an alias, but will be removed in a future release. [#6250](https://github.com/scalableminds/webknossos/pull/6250)
2125
- Slimmed the URLs for annotations by removing `Explorational` and `Task`. The old URLs are still supported, but will be redirected to the new format. [#6208](https://github.com/scalableminds/webknossos/pull/6208)
2226
- When creating a task from a base annotation, the starting position/rotation and bounding box as specified during task creation are now used and overwrite the ones from the original base annotation. [#6249](https://github.com/scalableminds/webknossos/pull/6249)
27+
- Increased maximum interpolation depth from 8 to 100. [#6292](https://github.com/scalableminds/webknossos/pull/6292)
2328

2429
### Fixed
25-
2630
- Fixed that bounding boxes were deletable in read-only tracings although the delete button was disabled. [#6273](https://github.com/scalableminds/webknossos/pull/6273)
2731
- Fixed that (old) sharing links with tokens did not work, because the token was removed during a redirection. [#6281](https://github.com/scalableminds/webknossos/pull/6281)
2832

MIGRATIONS.unreleased.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
88
## Unreleased
99
[Commits](https://github.com/scalableminds/webknossos/compare/22.06.1...HEAD)
1010

11+
- FossilDB now has to be started with two new additional column families: editableMappings,editableMappingUpdates. Note that this upgrade can not be trivially rolled back, since new rocksDB column families are added and it is not easy to remove them again from an existing database. In case webKnossos needs to be rolled back, it is recommended to still keep the new column families in FossilDB. [#6195](https://github.com/scalableminds/webknossos/pull/6195)
12+
1113
### Postgres Evolutions:

app/controllers/AnnotationIOController.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,9 @@ Expects:
428428

429429
def exportMimeTypeForAnnotation(annotation: Annotation): String =
430430
if (annotation.tracingType == TracingType.skeleton)
431-
"application/xml"
431+
xmlMimeType
432432
else
433-
"application/zip"
433+
zipMimeType
434434

435435
for {
436436
annotation <- provider.provideAnnotation(typ, annotationId, issuingUser) ~> NOT_FOUND

app/controllers/DataSetController.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class DataSetController @Inject()(userService: UserService,
116116
dataLayerName) ~> NOT_FOUND
117117
image <- imageFromCacheIfPossible(dataSet)
118118
} yield {
119-
addRemoteOriginHeaders(Ok(image)).as("image/jpeg").withHeaders(CACHE_CONTROL -> "public, max-age=86400")
119+
addRemoteOriginHeaders(Ok(image)).as(jpegMimeType).withHeaders(CACHE_CONTROL -> "public, max-age=86400")
120120
}
121121
}
122122

app/controllers/SitemapController.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class SitemapController @Inject()(sitemapWriter: SitemapWriter, sil: Silhouette[
1515
val downloadStream = sitemapWriter.toSitemapStream(prefix)
1616

1717
Ok.chunked(Source.fromPublisher(IterateeStreams.enumeratorToPublisher(downloadStream)))
18-
.as("application/xml")
18+
.as(xmlMimeType)
1919
.withHeaders(CONTENT_DISPOSITION ->
2020
"""sitemap.xml""")
2121
}

app/models/annotation/AnnotationService.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ class AnnotationService @Inject()(
833833
}
834834

835835
//for Explorative Annotations list
836-
def compactWrites(annotation: Annotation)(implicit ctx: DBAccessContext): Fox[JsObject] =
836+
def compactWrites(annotation: Annotation): Fox[JsObject] =
837837
for {
838838
dataSet <- dataSetDAO.findOne(annotation._dataSet)(GlobalAccessContext) ?~> "dataSet.notFoundForAnnotation"
839839
organization <- organizationDAO.findOne(dataSet._organization)(GlobalAccessContext) ?~> "organization.notFound"

conf/application.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ datastore {
145145
address = "localhost"
146146
port = 6379
147147
}
148-
agglomerateSkeleton.maxEdges = 10000
148+
agglomerateSkeleton.maxEdges = 100000
149149
}
150150

151151
# Proxy some routes to prefix + route (only if features.isDemoInstance, route "/" only if logged out)

conf/messages

+2
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ annotation.reopen.failed=Failed to reopen the annotation.
277277
annotation.sandbox.skeletonOnly=Sandbox annotations are currently available as skeleton only.
278278
annotation.multiLayers.skeleton.notImplemented=This feature is not implemented for annotations with more than one skeleton layer
279279
annotation.multiLayers.volume.notImplemented=This feature is not implemented for annotations with more than one volume layer
280+
annotation.noMappingSet=No mapping is pinned for this annotation, cannot generate agglomerate skeleton.
281+
annotation.volumeBucketsNotEmpty=Cannot make mapping editable in an annotation with mutated volume data
280282

281283
mesh.notFound=Mesh couldn’t be found
282284
mesh.write.failed=Failed to convert mesh info to json

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ services:
265265
command:
266266
- fossildb
267267
- -c
268-
- skeletons,skeletonUpdates,volumes,volumeData,volumeUpdates
268+
- skeletons,skeletonUpdates,volumes,volumeData,volumeUpdates,editableMappings,editableMappingUpdates
269269
user: ${USER_UID:-fossildb}:${USER_GID:-fossildb}
270270

271271
fossildb-persisted:

fossildb/run.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ if [ ! -f "$JAR" ] || [ ! "$CURRENT_VERSION" == "$VERSION" ]; then
1414
wget -q --show-progress -O "$JAR" "$URL"
1515
fi
1616

17-
COLLECTIONS="skeletons,skeletonUpdates,volumes,volumeData,volumeUpdates"
17+
COLLECTIONS="skeletons,skeletonUpdates,volumes,volumeData,volumeUpdates,editableMappings,editableMappingUpdates"
1818

1919
exec java -jar "$JAR" -c "$COLLECTIONS" -d "$FOSSILDB_HOME/data" -b "$FOSSILDB_HOME/backup"

frontend/javascripts/admin/admin_rest_api.ts

+49-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import type {
5454
ServerTracing,
5555
TracingType,
5656
WkConnectDatasetConfig,
57+
ServerEditableMapping,
5758
APICompoundType,
5859
} from "types/api_flow_types";
5960
import { APIAnnotationTypeEnum } from "types/api_flow_types";
@@ -81,6 +82,7 @@ import Toast from "libs/toast";
8182
import * as Utils from "libs/utils";
8283
import messages from "messages";
8384
import window, { location } from "libs/window";
85+
import { SaveQueueType } from "oxalis/model/actions/save_actions";
8486

8587
const MAX_SERVER_ITEMS_PER_RESPONSE = 1000;
8688

@@ -854,11 +856,11 @@ export async function getTracingForAnnotationType(
854856
export function getUpdateActionLog(
855857
tracingStoreUrl: string,
856858
tracingId: string,
857-
tracingType: "skeleton" | "volume",
859+
versionedObjectType: SaveQueueType,
858860
): Promise<Array<APIUpdateActionBatch>> {
859861
return doWithToken((token) =>
860862
Request.receiveJSON(
861-
`${tracingStoreUrl}/tracings/${tracingType}/${tracingId}/updateActionLog?token=${token}`,
863+
`${tracingStoreUrl}/tracings/${versionedObjectType}/${tracingId}/updateActionLog?token=${token}`,
862864
),
863865
);
864866
}
@@ -1584,6 +1586,29 @@ export function fetchMapping(
15841586
);
15851587
}
15861588

1589+
export function makeMappingEditable(
1590+
tracingStoreUrl: string,
1591+
tracingId: string,
1592+
): Promise<ServerEditableMapping> {
1593+
return doWithToken((token) =>
1594+
Request.receiveJSON(
1595+
`${tracingStoreUrl}/tracings/volume/${tracingId}/makeMappingEditable?token=${token}`,
1596+
{
1597+
method: "POST",
1598+
},
1599+
),
1600+
);
1601+
}
1602+
1603+
export function getEditableMapping(
1604+
tracingStoreUrl: string,
1605+
tracingId: string,
1606+
): Promise<ServerEditableMapping> {
1607+
return doWithToken((token) =>
1608+
Request.receiveJSON(`${tracingStoreUrl}/tracings/mapping/${tracingId}?token=${token}`),
1609+
);
1610+
}
1611+
15871612
export async function getAgglomeratesForDatasetLayer(
15881613
datastoreUrl: string,
15891614
datasetId: APIDatasetId,
@@ -1871,10 +1896,12 @@ export function getMeshData(id: string): Promise<ArrayBuffer> {
18711896
// These parameters are bundled into an object to avoid that the computeIsosurface function
18721897
// receives too many parameters, since this doesn't play well with the saga typings.
18731898
type IsosurfaceRequest = {
1899+
// The position is in voxels in mag 1
18741900
position: Vector3;
18751901
mag: Vector3;
18761902
segmentId: number;
18771903
subsamplingStrides: Vector3;
1904+
// The cubeSize is in voxels in mag <mag>
18781905
cubeSize: Vector3;
18791906
scale: Vector3;
18801907
mappingName: string | null | undefined;
@@ -1938,7 +1965,26 @@ export function getAgglomerateSkeleton(
19381965
return doWithToken((token) =>
19391966
Request.receiveArraybuffer(
19401967
`${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/agglomerates/${mappingId}/skeleton/${agglomerateId}?token=${token}`, // The webworker code cannot do proper error handling and always expects an array buffer from the server.
1941-
// In this case, the server sends an error json instead of an array buffer sometimes. Therefore, don't use the webworker code.
1968+
// The webworker code cannot do proper error handling and always expects an array buffer from the server.
1969+
// However, the server might send an error json instead of an array buffer. Therefore, don't use the webworker code.
1970+
{
1971+
useWebworkerForArrayBuffer: false,
1972+
showErrorToast: false,
1973+
},
1974+
),
1975+
);
1976+
}
1977+
1978+
export function getEditableAgglomerateSkeleton(
1979+
tracingStoreUrl: string,
1980+
tracingId: string,
1981+
agglomerateId: number,
1982+
): Promise<ArrayBuffer> {
1983+
return doWithToken((token) =>
1984+
Request.receiveArraybuffer(
1985+
`${tracingStoreUrl}/tracings/volume/${tracingId}/agglomerateSkeleton/${agglomerateId}?token=${token}`,
1986+
// The webworker code cannot do proper error handling and always expects an array buffer from the server.
1987+
// However, the server might send an error json instead of an array buffer. Therefore, don't use the webworker code.
19421988
{
19431989
useWebworkerForArrayBuffer: false,
19441990
showErrorToast: false,

frontend/javascripts/libs/format_utils.ts

+39-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { presetPalettes } from "@ant-design/colors";
33
import type { Vector3, Vector6 } from "oxalis/constants";
44
import { Unicode } from "oxalis/constants";
55
import * as Utils from "libs/utils";
6+
import _ from "lodash";
67
import type { BoundingBoxObject } from "oxalis/store";
78
const { ThinSpace, MultiplicationSymbol } = Unicode;
89
const COLOR_MAP: Array<string> = [
@@ -72,6 +73,24 @@ export function formatScale(scaleArr: Vector3 | null | undefined, roundTo: numbe
7273
return "";
7374
}
7475
}
76+
77+
export function formatNumberToUnit(number: number, unitMap: Map<number, string>): string {
78+
const closestFactor = findClosestToUnitFactor(number, unitMap);
79+
const unit = unitMap.get(closestFactor);
80+
81+
if (unit == null) {
82+
throw new Error("Couldn't look up appropriate unit.");
83+
}
84+
85+
const valueInUnit = number / closestFactor;
86+
87+
if (valueInUnit !== Math.floor(valueInUnit)) {
88+
return `${valueInUnit.toFixed(1)}${ThinSpace}${unit}`;
89+
}
90+
91+
return `${valueInUnit}${ThinSpace}${unit}`;
92+
}
93+
7594
const nmFactorToUnit = new Map([
7695
[1e-3, "pm"],
7796
[1, "nm"],
@@ -80,28 +99,31 @@ const nmFactorToUnit = new Map([
8099
[1e9, "m"],
81100
[1e12, "km"],
82101
]);
83-
const sortedNmFactors = Array.from(nmFactorToUnit.keys()).sort((a, b) => a - b);
84102
export function formatNumberToLength(lengthInNm: number): string {
85-
const closestFactor = findClosestLengthUnitFactor(lengthInNm);
86-
const unit = nmFactorToUnit.get(closestFactor);
87-
88-
if (unit == null) {
89-
throw new Error("Couldn't look up appropriate length unit.");
90-
}
103+
return formatNumberToUnit(lengthInNm, nmFactorToUnit);
104+
}
91105

92-
const lengthInUnit = lengthInNm / closestFactor;
106+
const byteFactorToUnit = new Map([
107+
[1, "B"],
108+
[1e3, "KB"],
109+
[1e6, "MB"],
110+
[1e9, "GB"],
111+
[1e12, "TB"],
112+
]);
113+
export function formatCountToDataAmountUnit(count: number): string {
114+
return formatNumberToUnit(count, byteFactorToUnit);
115+
}
93116

94-
if (lengthInUnit !== Math.floor(lengthInUnit)) {
95-
return `${lengthInUnit.toFixed(1)}${ThinSpace}${unit}`;
96-
}
117+
const getSortedFactors = _.memoize((unitMap: Map<number, string>) =>
118+
Array.from(unitMap.keys()).sort((a, b) => a - b),
119+
);
97120

98-
return `${lengthInUnit}${ThinSpace}${unit}`;
99-
}
100-
export function findClosestLengthUnitFactor(lengthInNm: number): number {
101-
let closestFactor = sortedNmFactors[0];
121+
export function findClosestToUnitFactor(number: number, unitMap: Map<number, string>): number {
122+
const sortedFactors = getSortedFactors(unitMap);
123+
let closestFactor = sortedFactors[0];
102124

103-
for (const factor of sortedNmFactors) {
104-
if (lengthInNm >= factor) {
125+
for (const factor of sortedFactors) {
126+
if (number >= factor) {
105127
closestFactor = factor;
106128
}
107129
}

frontend/javascripts/libs/window.ts

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const dummyLocation = {
5555
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Location | { ancestorOrigins: never[]; hash:... Remove this comment to see the full error message
5656
export const location: Location = typeof window === "undefined" ? dummyLocation : window.location;
5757

58+
let performanceCounterForMocking = 0;
59+
5860
const _window =
5961
typeof window === "undefined"
6062
? {
@@ -72,6 +74,7 @@ const _window =
7274
addEventListener,
7375
removeEventListener,
7476
open: (_url: string) => {},
77+
performance: { now: () => ++performanceCounterForMocking },
7578
}
7679
: window;
7780

frontend/javascripts/messages.ts

+3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ In order to restore the current window, a reload is necessary.`,
113113
"undo.no_undo":
114114
"There is no action that could be undone. However, if you want to restore an earlier version of this annotation, use the 'Restore Older Version' functionality in the dropdown next to the 'Save' button.",
115115
"undo.no_redo": "There is no action that could be redone.",
116+
"undo.no_undo_during_proofread":
117+
"Undo is not supported during proofreading yet. Please manually revert the last action you took.",
118+
"undo.no_redo_during_proofread": "Redo is not supported during proofreading yet.",
116119
"undo.import_volume_tracing":
117120
"Importing a volume annotation cannot be undone. However, if you want to restore an earlier version of this annotation, use the 'Restore Older Version' functionality in the dropdown next to the 'Save' button.",
118121
"download.wait": "Please wait...",

frontend/javascripts/oxalis/api/api_latest.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,11 @@ class DataApi {
15281528
});
15291529
}
15301530

1531-
getRawDataCuboid(layerName: string, topLeft: Vector3, bottomRight: Vector3): Promise<void> {
1531+
getRawDataCuboid(
1532+
layerName: string,
1533+
topLeft: Vector3,
1534+
bottomRight: Vector3,
1535+
): Promise<ArrayBuffer> {
15321536
return doWithToken((token) => {
15331537
const downloadUrl = this._getDownloadUrlForRawDataCuboid(
15341538
layerName,

frontend/javascripts/oxalis/constants.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export enum AnnotationToolEnum {
180180
FILL_CELL = "FILL_CELL",
181181
PICK_CELL = "PICK_CELL",
182182
BOUNDING_BOX = "BOUNDING_BOX",
183+
PROOFREAD = "PROOFREAD",
183184
}
184185
export const VolumeTools: Array<keyof typeof AnnotationToolEnum> = [
185186
AnnotationToolEnum.BRUSH,
@@ -257,7 +258,7 @@ export type ShowContextMenuFunction = (
257258
arg1: number,
258259
arg2: number | null | undefined,
259260
arg3: number | null | undefined,
260-
arg4: Vector3,
261+
arg4: Vector3 | null | undefined,
261262
arg5: OrthoView,
262263
) => void;
263264
const Constants = {
@@ -294,6 +295,8 @@ const Constants = {
294295
DEFAULT_NODE_RADIUS: 1.0,
295296
RESIZE_THROTTLE_TIME: 50,
296297
MIN_TREE_ID: 1,
298+
// TreeIds > 1024^2 break webKnossos, see https://github.com/scalableminds/webknossos/issues/5009
299+
MAX_TREE_ID: 1048576,
297300
MIN_NODE_ID: 1,
298301
// Maximum of how many buckets will be held in RAM (per layer)
299302
MAXIMUM_BUCKET_COUNT_PER_LAYER: 5000,

frontend/javascripts/oxalis/controller/combinations/bounding_box_handlers.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { calculateGlobalPos } from "oxalis/model/accessors/view_mode_accessor";
1+
import {
2+
calculateGlobalPos,
3+
calculateMaybeGlobalPos,
4+
} from "oxalis/model/accessors/view_mode_accessor";
25
import _ from "lodash";
36
import type { OrthoView, Point2, Vector3, BoundingBoxType } from "oxalis/constants";
47
import Store from "oxalis/store";
@@ -140,7 +143,10 @@ export function getClosestHoveredBoundingBox(
140143
plane: OrthoView,
141144
): [SelectedEdge, SelectedEdge | null | undefined] | null {
142145
const state = Store.getState();
143-
const globalPosition = calculateGlobalPos(state, pos, plane);
146+
const globalPosition = calculateMaybeGlobalPos(state, pos, plane);
147+
148+
if (globalPosition == null) return null;
149+
144150
const { userBoundingBoxes } = getSomeTracing(state.tracing);
145151
const indices = Dimension.getIndices(plane);
146152
const planeRatio = getBaseVoxelFactors(state.dataset.dataSource.scale);

0 commit comments

Comments
 (0)