@sjcrh/proteinpaint-server 2.187.0 → 2.188.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dataset/termdb.test.js +3 -3
- package/package.json +6 -6
- package/routes/aiProjectAdmin.js +2 -0
- package/routes/aiProjectSelectedWSImages.js +83 -43
- package/routes/brainImaging.js +1 -1
- package/routes/deleteWSITileSelection.js +48 -15
- package/routes/genesetEnrichment.js +23 -0
- package/routes/saveWSIAnnotation.js +75 -34
- package/routes/termdb.chat3.js +77 -59
- package/routes/termdb.cluster.js +4 -1
- package/routes/termdb.config.js +3 -0
- package/routes/termdb.dapVolcano.js +80 -0
- package/routes/termdb.proteome.js +14 -7
- package/routes/termdb.sampleScatter.js +4 -0
- package/routes/termdb.singlecellSamples.js +11 -0
- package/routes/termdb.topVariablyExpressedGenes.js +16 -142
- package/src/app.js +3172 -1376
package/dataset/termdb.test.js
CHANGED
|
@@ -244,6 +244,8 @@ function termdb_test_default() {
|
|
|
244
244
|
plots: [
|
|
245
245
|
{
|
|
246
246
|
name: "TermdbTest TSNE",
|
|
247
|
+
description: "Transcriptome t-SNE plot based on termdb data which is a 2D or 3D visualization of gene expression data where each point represents a single cell (or sample) and the distance between points reflects how similar their gene expression profiles are.",
|
|
248
|
+
descriptionShort: "transcriptome t-SNE plot",
|
|
247
249
|
dimension: 2,
|
|
248
250
|
file: "files/hg38/TermdbTest/tsne.txt",
|
|
249
251
|
colorTW: { id: "diaggrp" },
|
|
@@ -400,9 +402,7 @@ function termdb_test_default() {
|
|
|
400
402
|
unit: "M-value"
|
|
401
403
|
}
|
|
402
404
|
},
|
|
403
|
-
topVariablyExpressedGenes: {
|
|
404
|
-
src: "native"
|
|
405
|
-
},
|
|
405
|
+
topVariablyExpressedGenes: {},
|
|
406
406
|
rnaseqGeneCount: {
|
|
407
407
|
storage_type: "HDF5",
|
|
408
408
|
file: "files/hg38/TermdbTest/rnaseq/TermdbTest.geneCounts.new.h5"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.188.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@sjcrh/augen": "2.181.1",
|
|
65
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
66
|
-
"@sjcrh/proteinpaint-r": "2.
|
|
67
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
68
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
69
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
65
|
+
"@sjcrh/proteinpaint-python": "2.188.0",
|
|
66
|
+
"@sjcrh/proteinpaint-r": "2.188.0",
|
|
67
|
+
"@sjcrh/proteinpaint-rust": "2.188.0",
|
|
68
|
+
"@sjcrh/proteinpaint-shared": "2.188.0",
|
|
69
|
+
"@sjcrh/proteinpaint-types": "2.188.0",
|
|
70
70
|
"@types/express": "^5.0.0",
|
|
71
71
|
"@types/express-session": "^1.18.1",
|
|
72
72
|
"better-sqlite3": "^12.4.1",
|
package/routes/aiProjectAdmin.js
CHANGED
|
@@ -146,7 +146,9 @@ function editProject(connection, project) {
|
|
|
146
146
|
function deleteProject(connection, projectId) {
|
|
147
147
|
if (!projectId) throw new Error("Invalid project ID [aiProjectAdmin route deleteProject()]");
|
|
148
148
|
const stmts = [
|
|
149
|
+
{ sql: "DELETE FROM project_flagged_annotations WHERE project_id = ?", params: [[projectId]] },
|
|
149
150
|
{ sql: "DELETE FROM project_annotations WHERE project_id = ?", params: [[projectId]] },
|
|
151
|
+
{ sql: "DELETE FROM project_flagged_predictions WHERE project_id = ?", params: [[projectId]] },
|
|
150
152
|
{ sql: "DELETE FROM project_classes WHERE project_id = ?", params: [[projectId]] },
|
|
151
153
|
{ sql: "DELETE FROM project_images WHERE project_id = ?", params: [[projectId]] },
|
|
152
154
|
{ sql: "DELETE FROM project_users WHERE project_id = ?", params: [[projectId]] },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { aiProjectSelectedWSImagesResponsePayload } from "#types/checkers";
|
|
2
|
+
import { createSelectionID, SelectionPrefixes, FlagStatus } from "#shared";
|
|
2
3
|
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
4
|
const api = {
|
|
4
5
|
endpoint: "aiProjectSelectedWSImages",
|
|
@@ -54,16 +55,23 @@ function init({ genomes }) {
|
|
|
54
55
|
return `${zc[0]},${zc[1]}`;
|
|
55
56
|
}).filter(Boolean)
|
|
56
57
|
);
|
|
57
|
-
let flaggedPredictions = /* @__PURE__ */ new
|
|
58
|
+
let flaggedPredictions = /* @__PURE__ */ new Map();
|
|
58
59
|
if (typeof ds.queries.WSImages.getFlaggedPredictions === "function") {
|
|
59
|
-
flaggedPredictions = await ds.queries.WSImages.getFlaggedPredictions(projectId);
|
|
60
|
+
flaggedPredictions = await ds.queries.WSImages.getFlaggedPredictions(projectId, wsimageFilename);
|
|
60
61
|
}
|
|
61
62
|
wsimage.predictions = (predictions || []).map((p) => {
|
|
62
63
|
const label = classMap.get(p.class) ?? p.class;
|
|
63
|
-
|
|
64
|
+
const flagged_counterpart = flaggedPredictions.get(JSON.stringify(p.zoomCoordinates));
|
|
65
|
+
return {
|
|
66
|
+
...p,
|
|
67
|
+
class: label,
|
|
68
|
+
timestamp: flagged_counterpart?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
69
|
+
flag: flagged_counterpart?.flag ?? FlagStatus.Normal,
|
|
70
|
+
id: createSelectionID(SelectionPrefixes.Prediction, p.zoomCoordinates)
|
|
71
|
+
};
|
|
64
72
|
}).filter((p) => {
|
|
65
73
|
const key = `${p.zoomCoordinates[0]},${p.zoomCoordinates[1]}`;
|
|
66
|
-
return !annotationKeys.has(key) &&
|
|
74
|
+
return !annotationKeys.has(key) && p.flag !== FlagStatus.Deleted;
|
|
67
75
|
});
|
|
68
76
|
}
|
|
69
77
|
wsimages.push(wsimage);
|
|
@@ -91,23 +99,43 @@ async function validate_query_getWSIClassesQuery(ds) {
|
|
|
91
99
|
function validateWSIAnnotationsQuery(ds, connection) {
|
|
92
100
|
if (!ds.queries?.WSImages?.db) return;
|
|
93
101
|
const GET_ANNOTATIONS_SQL = `
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
SELECT *
|
|
103
|
+
FROM (
|
|
104
|
+
SELECT
|
|
105
|
+
pa.id,
|
|
106
|
+
pa.project_id,
|
|
107
|
+
pa.user_id,
|
|
108
|
+
pa.coordinates,
|
|
109
|
+
pa.timestamp,
|
|
110
|
+
pa.flagged,
|
|
111
|
+
pc.label AS label
|
|
112
|
+
FROM project_flagged_annotations pa
|
|
113
|
+
INNER JOIN project_images pi
|
|
114
|
+
ON pi.id = pa.image_id
|
|
115
|
+
LEFT JOIN project_classes pc
|
|
116
|
+
ON pc.id = pa.class_id
|
|
117
|
+
WHERE pa.project_id = ?
|
|
118
|
+
AND pi.image_path = ?
|
|
119
|
+
|
|
120
|
+
UNION ALL
|
|
121
|
+
|
|
122
|
+
SELECT
|
|
123
|
+
pa.id,
|
|
124
|
+
pa.project_id,
|
|
125
|
+
pa.user_id,
|
|
126
|
+
pa.coordinates,
|
|
127
|
+
pa.timestamp,
|
|
128
|
+
${FlagStatus.Normal} AS flagged,
|
|
129
|
+
pc.label AS label
|
|
130
|
+
FROM project_annotations pa
|
|
131
|
+
INNER JOIN project_images pi
|
|
132
|
+
ON pi.id = pa.image_id
|
|
133
|
+
LEFT JOIN project_classes pc
|
|
134
|
+
ON pc.id = pa.class_id
|
|
135
|
+
WHERE pa.project_id = ?
|
|
136
|
+
AND pi.image_path = ?
|
|
137
|
+
)
|
|
138
|
+
ORDER BY timestamp DESC, id DESC
|
|
111
139
|
`;
|
|
112
140
|
const GET_PROJECT_IMAGES_SQL = `
|
|
113
141
|
SELECT DISTINCT pi.image_path AS image_path
|
|
@@ -116,16 +144,22 @@ function validateWSIAnnotationsQuery(ds, connection) {
|
|
|
116
144
|
ORDER BY pi.id
|
|
117
145
|
`;
|
|
118
146
|
const GET_FLAGGED_PREDICTIONS_SQL = `
|
|
119
|
-
SELECT
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
SELECT
|
|
148
|
+
pa.coordinates,
|
|
149
|
+
pa.flag_type,
|
|
150
|
+
pa.timestamp
|
|
151
|
+
FROM project_flagged_predictions pa
|
|
152
|
+
INNER JOIN project_images pi
|
|
153
|
+
ON pi.id = pa.image_id
|
|
154
|
+
WHERE pa.project_id = ?
|
|
155
|
+
AND pa.image_id = ?
|
|
122
156
|
`;
|
|
123
157
|
if (!ds.queries) ds.queries = {};
|
|
124
158
|
if (!ds.queries.WSImages) ds.queries.WSImages = {};
|
|
125
159
|
ds.queries.WSImages.getWSIAnnotations = async (projectId, filename) => {
|
|
126
160
|
try {
|
|
127
161
|
const stmt = connection.prepare(GET_ANNOTATIONS_SQL);
|
|
128
|
-
const rows = stmt.all(projectId, filename);
|
|
162
|
+
const rows = stmt.all(projectId, filename, projectId, filename);
|
|
129
163
|
return rows.map((r) => {
|
|
130
164
|
let coords = [NaN, NaN];
|
|
131
165
|
try {
|
|
@@ -141,7 +175,9 @@ function validateWSIAnnotationsQuery(ds, connection) {
|
|
|
141
175
|
zoomCoordinates: coords,
|
|
142
176
|
class: r.label ?? "",
|
|
143
177
|
status: r.status,
|
|
144
|
-
|
|
178
|
+
flag: r.flagged === void 0 ? FlagStatus.Normal : r.flagged,
|
|
179
|
+
timestamp: r.timestamp,
|
|
180
|
+
id: createSelectionID(SelectionPrefixes.Annotation, coords)
|
|
145
181
|
};
|
|
146
182
|
});
|
|
147
183
|
} catch (error) {
|
|
@@ -159,28 +195,32 @@ function validateWSIAnnotationsQuery(ds, connection) {
|
|
|
159
195
|
return [];
|
|
160
196
|
}
|
|
161
197
|
};
|
|
162
|
-
ds.queries.WSImages.getFlaggedPredictions = async (projectId) => {
|
|
198
|
+
ds.queries.WSImages.getFlaggedPredictions = async (projectId, filename) => {
|
|
199
|
+
const predictionMap = /* @__PURE__ */ new Map();
|
|
200
|
+
const getImageIdSql = `
|
|
201
|
+
SELECT id
|
|
202
|
+
FROM project_images
|
|
203
|
+
WHERE project_id = ?
|
|
204
|
+
AND image_path = ?
|
|
205
|
+
LIMIT 1
|
|
206
|
+
`;
|
|
207
|
+
const getImageStmt = connection.prepare(getImageIdSql);
|
|
208
|
+
const imageRow = getImageStmt.get(projectId, filename);
|
|
209
|
+
if (!imageRow?.id) {
|
|
210
|
+
return predictionMap;
|
|
211
|
+
}
|
|
212
|
+
const imageId = Math.floor(imageRow.id);
|
|
163
213
|
try {
|
|
164
214
|
const stmt = connection.prepare(GET_FLAGGED_PREDICTIONS_SQL);
|
|
165
|
-
const rows = stmt.all(projectId) || [];
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const parsed = typeof r.coordinates === "string" ? JSON.parse(r.coordinates) : r.coordinates;
|
|
170
|
-
if (Array.isArray(parsed) && parsed.length >= 2) {
|
|
171
|
-
const x = Number(parsed[0]);
|
|
172
|
-
const y = Number(parsed[1]);
|
|
173
|
-
if (!Number.isNaN(x) && !Number.isNaN(y)) {
|
|
174
|
-
keys.add(`${x},${y}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
215
|
+
const rows = stmt.all(projectId, imageId) || [];
|
|
216
|
+
for (const p of rows) {
|
|
217
|
+
if (p.coordinates && typeof p.coordinates === "string" && typeof p.flag_type === "number")
|
|
218
|
+
predictionMap.set(p.coordinates, { flag: p.flag_type, timestamp: p.timestamp });
|
|
179
219
|
}
|
|
180
|
-
return
|
|
220
|
+
return predictionMap;
|
|
181
221
|
} catch (error) {
|
|
182
222
|
console.error("Error loading flagged predictions:", error);
|
|
183
|
-
return
|
|
223
|
+
return predictionMap;
|
|
184
224
|
}
|
|
185
225
|
};
|
|
186
226
|
}
|
package/routes/brainImaging.js
CHANGED
|
@@ -3,7 +3,7 @@ import serverconfig from "#src/serverconfig.js";
|
|
|
3
3
|
import { brainImagingPayload } from "#types/checkers";
|
|
4
4
|
import { getData } from "../src/termdb.matrix.js";
|
|
5
5
|
import { isNumericTerm } from "#shared";
|
|
6
|
-
import { getColors } from "#shared
|
|
6
|
+
import { getColors } from "#shared";
|
|
7
7
|
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
8
8
|
const api = {
|
|
9
9
|
endpoint: "brainImaging",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deleteWSITileSelectionPayload } from "#types/checkers";
|
|
2
|
+
import { checkSelectionType, SelectionPrefixes, FlagStatus } from "#shared";
|
|
2
3
|
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
4
|
const api = {
|
|
4
5
|
endpoint: `deleteWSITileSelection`,
|
|
@@ -48,7 +49,8 @@ async function validate_query_deleteWSIAnnotation(ds) {
|
|
|
48
49
|
}
|
|
49
50
|
function validateQuery(ds, connection) {
|
|
50
51
|
ds.queries.WSImages.deleteAnnotation = async (query) => {
|
|
51
|
-
|
|
52
|
+
const zoomCoordinates = JSON.stringify(query.tileSelection.zoomCoordinates);
|
|
53
|
+
if (checkSelectionType(query.tileSelection, SelectionPrefixes.Prediction) && query.tileSelection.flag !== FlagStatus.Normal) {
|
|
52
54
|
try {
|
|
53
55
|
const projectId = query.projectId;
|
|
54
56
|
if (projectId == null) {
|
|
@@ -57,21 +59,41 @@ function validateQuery(ds, connection) {
|
|
|
57
59
|
error: "Missing required field: projectId"
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
|
-
const predictionId = query.
|
|
61
|
-
const
|
|
62
|
-
const flagType = 0;
|
|
62
|
+
const predictionId = query.classID;
|
|
63
|
+
const flagType = query.tileSelection.flag;
|
|
63
64
|
if (predictionId == null) {
|
|
64
65
|
return {
|
|
65
66
|
status: "error",
|
|
66
67
|
error: "Missing prediction id in tileSelection (expected predictionId, prediction.id, or id)."
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
70
|
+
const getImageIdSql = `
|
|
71
|
+
SELECT id FROM project_images
|
|
72
|
+
WHERE project_id = ? AND image_path = ? LIMIT 1
|
|
73
|
+
`;
|
|
74
|
+
const imageRow = connection.prepare(getImageIdSql).get(projectId, query.wsimage);
|
|
75
|
+
const imageId = imageRow?.id;
|
|
76
|
+
if (!imageId) {
|
|
77
|
+
return { status: "error", error: "Image not found" };
|
|
78
|
+
}
|
|
79
|
+
connection.prepare(
|
|
80
|
+
`
|
|
81
|
+
DELETE FROM project_flagged_predictions
|
|
82
|
+
WHERE project_id = ? AND coordinates = ?
|
|
83
|
+
AND image_id = (
|
|
84
|
+
SELECT id FROM project_images
|
|
85
|
+
WHERE project_id = ?
|
|
86
|
+
AND image_path = ?
|
|
87
|
+
)
|
|
88
|
+
`
|
|
89
|
+
).run(projectId, zoomCoordinates, projectId, query.wsimage);
|
|
90
|
+
connection.prepare(
|
|
91
|
+
`
|
|
92
|
+
INSERT INTO project_flagged_predictions
|
|
93
|
+
(project_id, prediction_class_id, coordinates, flag_type, timestamp, image_id)
|
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
95
|
+
`
|
|
96
|
+
).run(projectId, predictionId, zoomCoordinates, flagType, (/* @__PURE__ */ new Date()).toISOString(), imageId);
|
|
75
97
|
return { status: "ok" };
|
|
76
98
|
} catch (error) {
|
|
77
99
|
console.error("Error inserting flagged prediction:", error);
|
|
@@ -82,7 +104,20 @@ function validateQuery(ds, connection) {
|
|
|
82
104
|
}
|
|
83
105
|
} else
|
|
84
106
|
try {
|
|
85
|
-
|
|
107
|
+
connection.prepare(
|
|
108
|
+
`
|
|
109
|
+
DELETE FROM project_flagged_annotations
|
|
110
|
+
WHERE project_id = ?
|
|
111
|
+
AND coordinates = ?
|
|
112
|
+
AND image_id = (
|
|
113
|
+
SELECT id FROM project_images
|
|
114
|
+
WHERE project_id = ?
|
|
115
|
+
AND image_path = ?
|
|
116
|
+
)
|
|
117
|
+
`
|
|
118
|
+
).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
|
|
119
|
+
connection.prepare(
|
|
120
|
+
`
|
|
86
121
|
DELETE FROM project_annotations
|
|
87
122
|
WHERE project_id = ?
|
|
88
123
|
AND coordinates = ?
|
|
@@ -91,10 +126,8 @@ function validateQuery(ds, connection) {
|
|
|
91
126
|
WHERE project_id = ?
|
|
92
127
|
AND image_path = ?
|
|
93
128
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const stmt = connection.prepare(deleteSql);
|
|
97
|
-
stmt.run(query.projectId, coords, query.projectId, query.wsimage);
|
|
129
|
+
`
|
|
130
|
+
).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
|
|
98
131
|
return { status: "ok" };
|
|
99
132
|
} catch (error) {
|
|
100
133
|
console.error("Error deleting annotation:", error);
|
|
@@ -8,6 +8,7 @@ import { run_rust } from "@sjcrh/proteinpaint-rust";
|
|
|
8
8
|
import { mayLog } from "#src/helpers.ts";
|
|
9
9
|
import { formatElapsedTime } from "#shared";
|
|
10
10
|
import { readCacheFileOrRecompute, stableStringify } from "#src/diffAnalysis.ts";
|
|
11
|
+
import { get_ds_tdb } from "#src/termdb.js";
|
|
11
12
|
const api = {
|
|
12
13
|
endpoint: "genesetEnrichment",
|
|
13
14
|
methods: {
|
|
@@ -100,6 +101,28 @@ async function resolveGseaGenesAndFoldChange({
|
|
|
100
101
|
}
|
|
101
102
|
throw new Error(`unexpected result kind: ${result.kind}`);
|
|
102
103
|
}
|
|
104
|
+
if (q.dapParams) {
|
|
105
|
+
const genome = genomes[q.genome];
|
|
106
|
+
if (!genome) throw new Error("invalid genome");
|
|
107
|
+
const [ds] = get_ds_tdb(genome, q);
|
|
108
|
+
const { organism, assay, cohort } = q.dapParams;
|
|
109
|
+
const cohortConfig = ds.queries?.proteome?.organisms?.[organism]?.assays?.[assay]?.cohorts?.[cohort];
|
|
110
|
+
if (!cohortConfig?.DAPfile) throw new Error("DAP file not configured for this cohort");
|
|
111
|
+
const filePath = path.join(serverconfig.tpmasterdir, cohortConfig.DAPfile);
|
|
112
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
113
|
+
const lines = content.trim().split("\n");
|
|
114
|
+
const genes = [];
|
|
115
|
+
const fold_change = [];
|
|
116
|
+
for (let i = 1; i < lines.length; i++) {
|
|
117
|
+
const parts = lines[i].split(" ");
|
|
118
|
+
if (parts.length < 4) continue;
|
|
119
|
+
const fc = Number(parts[2]);
|
|
120
|
+
if (!Number.isFinite(fc)) continue;
|
|
121
|
+
genes.push(parts[1]);
|
|
122
|
+
fold_change.push(fc);
|
|
123
|
+
}
|
|
124
|
+
return { genes, fold_change };
|
|
125
|
+
}
|
|
103
126
|
if (!q.genes || !q.fold_change) throw new Error("requires genes and fold_change when cacheId is absent");
|
|
104
127
|
return { genes: q.genes, fold_change: q.fold_change };
|
|
105
128
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { saveWSIAnnotationPayload } from "#types/checkers";
|
|
2
|
+
import { FlagStatus, SelectionPrefixes, checkSelectionType } from "#shared";
|
|
2
3
|
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
4
|
const routePath = "saveWSIAnnotation";
|
|
4
5
|
const api = {
|
|
@@ -47,12 +48,21 @@ async function validate_query_saveWSIAnnotation(ds) {
|
|
|
47
48
|
function validateQuery(ds, connection) {
|
|
48
49
|
ds.queries.WSImages.saveWSIAnnotation = async (annotation) => {
|
|
49
50
|
try {
|
|
51
|
+
const tileSelection = annotation.tileSelection;
|
|
50
52
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
51
53
|
const projectId = annotation.projectId;
|
|
52
54
|
const wsimageFilename = annotation.wsimage;
|
|
53
|
-
const coords = JSON.stringify(
|
|
54
|
-
const
|
|
55
|
+
const coords = JSON.stringify(tileSelection.zoomCoordinates ?? []);
|
|
56
|
+
const flag = tileSelection.flag;
|
|
55
57
|
const classId = annotation.classId;
|
|
58
|
+
const isAnnotation = checkSelectionType(tileSelection, SelectionPrefixes.Annotation);
|
|
59
|
+
const isPrediction = checkSelectionType(tileSelection, SelectionPrefixes.Prediction);
|
|
60
|
+
if (!isAnnotation && !isPrediction) {
|
|
61
|
+
return {
|
|
62
|
+
status: "error",
|
|
63
|
+
error: `Invalid tileSelection id "${tileSelection.id}". Must start with "${SelectionPrefixes.Annotation}" or "${SelectionPrefixes.Prediction}".`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
56
66
|
if (projectId == null || wsimageFilename == null) {
|
|
57
67
|
return {
|
|
58
68
|
status: "error",
|
|
@@ -75,41 +85,72 @@ function validateQuery(ds, connection) {
|
|
|
75
85
|
};
|
|
76
86
|
}
|
|
77
87
|
const imageId = imageRow.id;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const insertSql = `
|
|
100
|
-
INSERT INTO project_annotations (
|
|
101
|
-
project_id, user_id, coordinates, timestamp, status, class_id, image_id
|
|
102
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
103
|
-
`;
|
|
104
|
-
const insertStmt = connection.prepare(insertSql);
|
|
105
|
-
const userRow = connection.prepare(
|
|
106
|
-
`SELECT id
|
|
88
|
+
connection.prepare(
|
|
89
|
+
`DELETE FROM project_flagged_annotations
|
|
90
|
+
WHERE project_id = ?
|
|
91
|
+
AND image_id = ?
|
|
92
|
+
AND coordinates = ?`
|
|
93
|
+
).run(projectId, imageId, coords);
|
|
94
|
+
connection.prepare(
|
|
95
|
+
`DELETE FROM project_flagged_predictions
|
|
96
|
+
WHERE project_id = ?
|
|
97
|
+
AND image_id = ?
|
|
98
|
+
AND coordinates = ?`
|
|
99
|
+
).run(projectId, imageId, coords);
|
|
100
|
+
connection.prepare(
|
|
101
|
+
`DELETE FROM project_annotations
|
|
102
|
+
WHERE project_id = ?
|
|
103
|
+
AND image_id = ?
|
|
104
|
+
AND coordinates = ?`
|
|
105
|
+
).run(projectId, imageId, coords);
|
|
106
|
+
if (isAnnotation) {
|
|
107
|
+
const userRow = connection.prepare(
|
|
108
|
+
`SELECT id
|
|
107
109
|
FROM project_users
|
|
108
110
|
ORDER BY id
|
|
109
111
|
LIMIT 1`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
).get();
|
|
113
|
+
const userId = userRow?.id;
|
|
114
|
+
if (userId === void 0) {
|
|
115
|
+
return {
|
|
116
|
+
status: "error",
|
|
117
|
+
error: "No users found in project_users table."
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (tileSelection.flag === FlagStatus.Normal) {
|
|
121
|
+
connection.prepare(
|
|
122
|
+
`
|
|
123
|
+
INSERT INTO project_annotations (
|
|
124
|
+
project_id, user_id, coordinates, timestamp,class_id, image_id
|
|
125
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
126
|
+
`
|
|
127
|
+
).run(projectId, userId, coords, timestamp, classId, imageId);
|
|
128
|
+
} else {
|
|
129
|
+
connection.prepare(
|
|
130
|
+
`
|
|
131
|
+
INSERT INTO project_flagged_annotations (
|
|
132
|
+
project_id, user_id, coordinates, timestamp, flagged,class_id, image_id
|
|
133
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
134
|
+
`
|
|
135
|
+
).run(projectId, userId, coords, timestamp, flag, classId, imageId);
|
|
136
|
+
}
|
|
137
|
+
} else if (isPrediction) {
|
|
138
|
+
if (tileSelection.flag !== FlagStatus.Normal) {
|
|
139
|
+
const insertSql = `
|
|
140
|
+
INSERT INTO project_flagged_predictions (project_id, prediction_class_id, coordinates, flag_type,image_id,timestamp)
|
|
141
|
+
VALUES (?, ?, ?, ?,?,?)
|
|
142
|
+
`;
|
|
143
|
+
const insertStmt = connection.prepare(insertSql);
|
|
144
|
+
insertStmt.run(
|
|
145
|
+
annotation.projectId,
|
|
146
|
+
annotation.classId,
|
|
147
|
+
JSON.stringify(tileSelection.zoomCoordinates),
|
|
148
|
+
tileSelection.flag,
|
|
149
|
+
imageId,
|
|
150
|
+
timestamp
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
113
154
|
return { status: "ok" };
|
|
114
155
|
} catch (error) {
|
|
115
156
|
console.error("Error saving annotation:", error);
|