@sjcrh/proteinpaint-server 2.191.3 → 2.191.4
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 -0
- package/package.json +4 -4
- package/src/app.js +496 -411
- package/routes/aiProjectAdmin.js +0 -179
- package/routes/aiProjectSelectedWSImages.js +0 -237
- package/routes/deleteWSITileSelection.js +0 -142
- package/routes/saveWSIAnnotation.js +0 -151
- package/routes/wsimages.js +0 -176
package/routes/aiProjectAdmin.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
2
|
-
import { runMultiStmtSQL, runSQL } from "#src/runSQLHelpers.ts";
|
|
3
|
-
function init({ genomes }) {
|
|
4
|
-
return async (req, res) => {
|
|
5
|
-
try {
|
|
6
|
-
const query = req.query;
|
|
7
|
-
if (!query.genome || !query.dslabel) {
|
|
8
|
-
throw new Error("Genome and dataset label are required for aiProjectAdmin request.");
|
|
9
|
-
}
|
|
10
|
-
const g = genomes[query.genome];
|
|
11
|
-
const ds = g.datasets[query.dslabel];
|
|
12
|
-
if (!ds.queries?.WSImages?.db) throw new Error("WSImages database not found.");
|
|
13
|
-
const connection = getDbConnection(ds);
|
|
14
|
-
const aiHalAuth = ds.queries?.AIHalAuth;
|
|
15
|
-
if (!aiHalAuth) throw new Error("AIHalAuth queries not found in dataset.");
|
|
16
|
-
const userEmail = req.query.__protected__.clientAuthResult?.email || "";
|
|
17
|
-
if (query.for === "list") {
|
|
18
|
-
let projects = getProjects(connection);
|
|
19
|
-
if (!aiHalAuth.checkAuthorization(req, "listAllProjects")) {
|
|
20
|
-
projects = projects.filter((p) => aiHalAuth.getUsers(connection, p.id).includes(userEmail));
|
|
21
|
-
}
|
|
22
|
-
res.send(projects);
|
|
23
|
-
} else if (query.for === "admin") {
|
|
24
|
-
if (req.method === "PUT" || query.project.type === "new" && aiHalAuth.checkAuthorization(req, "addProject"))
|
|
25
|
-
addProject(connection, query.project);
|
|
26
|
-
else if (req.method === "POST" && aiHalAuth.checkAuthorization(req, "editProject")) {
|
|
27
|
-
editProject(connection, query.project);
|
|
28
|
-
aiHalAuth.setUser(connection, query.project.id, userEmail || "admin", 12);
|
|
29
|
-
} else if (req.method === "DELETE" && aiHalAuth.checkAuthorization(req, "deleteProject"))
|
|
30
|
-
deleteProject(connection, query.project.id);
|
|
31
|
-
else throw new Error('Invalid request method for="admin" in aiProjectAdmin route.');
|
|
32
|
-
let projectId = query.project.id;
|
|
33
|
-
if (!projectId) {
|
|
34
|
-
const row = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(query.project.name);
|
|
35
|
-
if (!row) {
|
|
36
|
-
throw new Error(`Project not found: ${query.project.name}`);
|
|
37
|
-
}
|
|
38
|
-
projectId = row.id;
|
|
39
|
-
}
|
|
40
|
-
res.status(200).send({
|
|
41
|
-
status: "ok",
|
|
42
|
-
projectId,
|
|
43
|
-
message: `Project ${query.project.name} processed successfully`
|
|
44
|
-
});
|
|
45
|
-
} else if (query.for === "filterImages") {
|
|
46
|
-
const parsedLogin = aiHalAuth.parseLogin(req, connection);
|
|
47
|
-
if (parsedLogin.status === "error") throw new Error("Authentication failed: " + parsedLogin.error);
|
|
48
|
-
const q = ds.cohort.termdb.q;
|
|
49
|
-
const data = await q.getFilteredImages(query.project.filter);
|
|
50
|
-
data.selectedImages = await ds.queries?.WSImages?.selectWSIImages();
|
|
51
|
-
res.status(200).send({
|
|
52
|
-
status: "ok",
|
|
53
|
-
data
|
|
54
|
-
});
|
|
55
|
-
} else if (query.for === "images") {
|
|
56
|
-
const parsedLogin = aiHalAuth.parseLogin(req, connection);
|
|
57
|
-
if (parsedLogin.status === "error") throw new Error("Authentication failed: " + parsedLogin.error);
|
|
58
|
-
const images = getImages(connection, query.project);
|
|
59
|
-
res.send({ images, status: "ok" });
|
|
60
|
-
} else if (query.for === "logout") {
|
|
61
|
-
aiHalAuth.setUser(connection, query.project.id, null);
|
|
62
|
-
res.status(200).send({
|
|
63
|
-
status: "ok",
|
|
64
|
-
message: "User logged out successfully"
|
|
65
|
-
});
|
|
66
|
-
} else if (query.for === "auth") {
|
|
67
|
-
const authorizations = {};
|
|
68
|
-
const actions = query.auth || [];
|
|
69
|
-
for (const action of actions) {
|
|
70
|
-
authorizations[action] = aiHalAuth.checkAuthorization(req, action);
|
|
71
|
-
}
|
|
72
|
-
res.status(200).send(authorizations);
|
|
73
|
-
} else {
|
|
74
|
-
res.send({
|
|
75
|
-
status: "error",
|
|
76
|
-
message: "Invalid request"
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
} catch (e) {
|
|
80
|
-
console.warn(e);
|
|
81
|
-
res.status(500).send({
|
|
82
|
-
status: "error",
|
|
83
|
-
error: e.message || e
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function getProjects(connection) {
|
|
89
|
-
const sql = "SELECT name, id, current_user FROM project";
|
|
90
|
-
return runSQL(connection, sql);
|
|
91
|
-
}
|
|
92
|
-
function getImages(connection, project) {
|
|
93
|
-
if (!project.id) {
|
|
94
|
-
const res = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(project.name);
|
|
95
|
-
if (!res) throw new Error(`Project not found for name: ${project.name}`);
|
|
96
|
-
project.id = res.id;
|
|
97
|
-
}
|
|
98
|
-
const imageRows = connection.prepare(`SELECT image_path FROM project_images WHERE project_id = ? ORDER BY id ASC`).all(project.id);
|
|
99
|
-
return imageRows.map((r) => r.image_path);
|
|
100
|
-
}
|
|
101
|
-
function editProject(connection, project) {
|
|
102
|
-
const stmts = [];
|
|
103
|
-
if (!project.id) {
|
|
104
|
-
const res = connection.prepare(`SELECT id FROM project WHERE name = ?`).get(project.name);
|
|
105
|
-
project.id = res.id;
|
|
106
|
-
}
|
|
107
|
-
if (project.images) {
|
|
108
|
-
stmts.push({
|
|
109
|
-
sql: `DELETE FROM project_images WHERE project_id = ? AND image_path NOT IN (${project.images.map(() => "?").join(",") || "''"})`,
|
|
110
|
-
params: [[project.id, ...project.images]]
|
|
111
|
-
});
|
|
112
|
-
const existingImg = connection.prepare(`SELECT 1 FROM project_images WHERE project_id = ? AND image_path = ?`);
|
|
113
|
-
const multiParams = [];
|
|
114
|
-
for (const img of project.images) {
|
|
115
|
-
const exists = existingImg.get(project.id, img);
|
|
116
|
-
if (!exists) multiParams.push([project.id, img]);
|
|
117
|
-
}
|
|
118
|
-
if (multiParams.length > 0) {
|
|
119
|
-
const insertImg = `INSERT INTO project_images (project_id, image_path) VALUES (?, ?)`;
|
|
120
|
-
stmts.push({ sql: insertImg, params: multiParams });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (project.filter) {
|
|
124
|
-
stmts.push({
|
|
125
|
-
sql: `UPDATE project SET filter = ? WHERE id = ?`,
|
|
126
|
-
params: [[JSON.stringify(project.filter), project.id]]
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (project.classes) {
|
|
130
|
-
stmts.push({
|
|
131
|
-
sql: `DELETE FROM project_classes WHERE project_id = ? AND name NOT IN (${project.classes.map(() => "?").join(",") || "''"})`,
|
|
132
|
-
params: [project.id, ...project.classes.map((c) => c.name)]
|
|
133
|
-
});
|
|
134
|
-
const existingClasses = connection.prepare(`SELECT 1 FROM project_classes WHERE project_id = ? AND name = ?`);
|
|
135
|
-
const multiParams = [];
|
|
136
|
-
for (const cls of project.classes) {
|
|
137
|
-
const exists = existingClasses.get(project.id, cls.name);
|
|
138
|
-
if (!exists) multiParams.push([project.id, cls.name, cls.color, cls.key_shortcut || ""]);
|
|
139
|
-
}
|
|
140
|
-
if (multiParams.length > 0) {
|
|
141
|
-
const insertClass = `INSERT INTO project_classes (project_id, name, color, key_shortcut) VALUES (?, ?, ?, ?)`;
|
|
142
|
-
stmts.push({ sql: insertClass, params: multiParams });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
runMultiStmtSQL(connection, stmts, "add");
|
|
146
|
-
}
|
|
147
|
-
function deleteProject(connection, projectId) {
|
|
148
|
-
if (!projectId) throw new Error("Invalid project ID [aiProjectAdmin route deleteProject()]");
|
|
149
|
-
const stmts = [
|
|
150
|
-
{ sql: "DELETE FROM project_flagged_annotations WHERE project_id = ?", params: [[projectId]] },
|
|
151
|
-
{ sql: "DELETE FROM project_annotations WHERE project_id = ?", params: [[projectId]] },
|
|
152
|
-
{ sql: "DELETE FROM project_flagged_predictions WHERE project_id = ?", params: [[projectId]] },
|
|
153
|
-
{ sql: "DELETE FROM project_classes WHERE project_id = ?", params: [[projectId]] },
|
|
154
|
-
{ sql: "DELETE FROM project_images WHERE project_id = ?", params: [[projectId]] },
|
|
155
|
-
{ sql: "DELETE FROM project_users WHERE project_id = ?", params: [[projectId]] },
|
|
156
|
-
{ sql: "DELETE FROM project WHERE id = ?", params: [[projectId]] }
|
|
157
|
-
];
|
|
158
|
-
runMultiStmtSQL(connection, stmts, "delete");
|
|
159
|
-
}
|
|
160
|
-
function addProject(connection, project) {
|
|
161
|
-
const projectSql = `INSERT INTO project (name, filter)
|
|
162
|
-
VALUES (?, ?)`;
|
|
163
|
-
const projectParams = [project.name, JSON.stringify(project.filter)];
|
|
164
|
-
const row = runSQL(connection, projectSql, projectParams, "add");
|
|
165
|
-
if (project.users) {
|
|
166
|
-
const userSql = `INSERT INTO project_users (project_id, email)
|
|
167
|
-
VALUES (?, ?)`;
|
|
168
|
-
const userParams = project.users.map((email) => [row.lastInsertRowid, email]);
|
|
169
|
-
runMultiStmtSQL(connection, [{ sql: userSql, params: userParams }], "add");
|
|
170
|
-
}
|
|
171
|
-
const classSql = `INSERT INTO project_classes (project_id, label, color, key_shortcut)
|
|
172
|
-
VALUES (?, ?, ?, ?)`;
|
|
173
|
-
const classParams = project.classes.map((c) => [row.lastInsertRowid, c.label, c.color, c.key_shortcut || ""]);
|
|
174
|
-
runMultiStmtSQL(connection, [{ sql: classSql, params: classParams }], "add");
|
|
175
|
-
}
|
|
176
|
-
export {
|
|
177
|
-
getImages,
|
|
178
|
-
init
|
|
179
|
-
};
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { createSelectionID, SelectionPrefixes, FlagStatus } from "#types";
|
|
2
|
-
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
|
-
function init({ genomes }) {
|
|
4
|
-
return async (req, res) => {
|
|
5
|
-
try {
|
|
6
|
-
const query = req.query;
|
|
7
|
-
const g = genomes[query.genome];
|
|
8
|
-
if (!g) throw "invalid genome name";
|
|
9
|
-
const ds = g.datasets[query.dslabel];
|
|
10
|
-
if (!ds) throw "invalid dataset name";
|
|
11
|
-
const projectId = query.projectId;
|
|
12
|
-
const wsimagesFilenamesArg = query.wsimagesFilenames;
|
|
13
|
-
let wsimagesFilenames = [];
|
|
14
|
-
const wsimages = [];
|
|
15
|
-
if (Array.isArray(wsimagesFilenamesArg) && wsimagesFilenamesArg.length === 1 && wsimagesFilenamesArg[0] === "all") {
|
|
16
|
-
if (ds.queries?.WSImages?.getAllWSImages) {
|
|
17
|
-
try {
|
|
18
|
-
wsimagesFilenames = await ds.queries.WSImages.getAllWSImages(projectId);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
console.error("Failed to get WSI image list via helper, falling back to provided param", e);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
} else {
|
|
24
|
-
wsimagesFilenames = wsimagesFilenamesArg;
|
|
25
|
-
}
|
|
26
|
-
if (ds.queries.WSImages.getWSIAnnotations) {
|
|
27
|
-
for (const wsimageFilename of wsimagesFilenames) {
|
|
28
|
-
const wsimage = {
|
|
29
|
-
filename: wsimageFilename
|
|
30
|
-
};
|
|
31
|
-
wsimage.annotations = await ds.queries.WSImages.getWSIAnnotations(projectId, wsimageFilename);
|
|
32
|
-
wsimage.classes = await ds.queries.WSImages.getAnnotationClasses(projectId);
|
|
33
|
-
wsimage.uncertainty = ds.queries?.WSImages?.uncertainty;
|
|
34
|
-
wsimage.activePatchColor = ds.queries?.WSImages?.activePatchColor;
|
|
35
|
-
if (ds.queries.WSImages.getWSIPredictionPatches) {
|
|
36
|
-
const predictions = await ds.queries.WSImages.getWSIPredictionPatches(projectId, wsimageFilename);
|
|
37
|
-
const classMap = new Map((wsimage.classes || []).map((c) => [c.id, c.label]));
|
|
38
|
-
const annotationKeys = new Set(
|
|
39
|
-
(wsimage.annotations || []).map((a) => {
|
|
40
|
-
const zc = a.zoomCoordinates || [NaN, NaN];
|
|
41
|
-
return `${zc[0]},${zc[1]}`;
|
|
42
|
-
}).filter(Boolean)
|
|
43
|
-
);
|
|
44
|
-
let flaggedPredictions = /* @__PURE__ */ new Map();
|
|
45
|
-
if (typeof ds.queries.WSImages.getFlaggedPredictions === "function") {
|
|
46
|
-
flaggedPredictions = await ds.queries.WSImages.getFlaggedPredictions(projectId, wsimageFilename);
|
|
47
|
-
}
|
|
48
|
-
wsimage.predictions = (predictions || []).map((p) => {
|
|
49
|
-
const label = classMap.get(p.class) ?? p.class;
|
|
50
|
-
const flagged_counterpart = flaggedPredictions.get(JSON.stringify(p.zoomCoordinates));
|
|
51
|
-
return {
|
|
52
|
-
...p,
|
|
53
|
-
class: label,
|
|
54
|
-
timestamp: flagged_counterpart?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
55
|
-
flag: flagged_counterpart?.flag ?? FlagStatus.Normal,
|
|
56
|
-
id: createSelectionID(SelectionPrefixes.Prediction, p.zoomCoordinates)
|
|
57
|
-
};
|
|
58
|
-
}).filter((p) => {
|
|
59
|
-
const key = `${p.zoomCoordinates[0]},${p.zoomCoordinates[1]}`;
|
|
60
|
-
return !annotationKeys.has(key) && p.flag !== FlagStatus.Deleted;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
wsimages.push(wsimage);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
res.send({ wsimages });
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.log(e);
|
|
69
|
-
res.status(404).send("Sample images not found");
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async function validate_query_getWSIAnnotations(ds) {
|
|
74
|
-
if (!ds.queries?.WSImages?.db) return;
|
|
75
|
-
const connection = getDbConnection(ds);
|
|
76
|
-
if (!connection) return;
|
|
77
|
-
validateWSIAnnotationsQuery(ds, connection);
|
|
78
|
-
}
|
|
79
|
-
async function validate_query_getWSIClassesQuery(ds) {
|
|
80
|
-
if (!ds.queries?.WSImages?.db) return;
|
|
81
|
-
const connection = getDbConnection(ds);
|
|
82
|
-
if (!connection) return;
|
|
83
|
-
validateWSIClassesQuery(ds, connection);
|
|
84
|
-
}
|
|
85
|
-
function validateWSIAnnotationsQuery(ds, connection) {
|
|
86
|
-
if (!ds.queries?.WSImages?.db) return;
|
|
87
|
-
const GET_ANNOTATIONS_SQL = `
|
|
88
|
-
SELECT *
|
|
89
|
-
FROM (
|
|
90
|
-
SELECT
|
|
91
|
-
pa.id,
|
|
92
|
-
pa.project_id,
|
|
93
|
-
pa.user_id,
|
|
94
|
-
pa.coordinates,
|
|
95
|
-
pa.timestamp,
|
|
96
|
-
pa.flagged,
|
|
97
|
-
pc.label AS label
|
|
98
|
-
FROM project_flagged_annotations pa
|
|
99
|
-
INNER JOIN project_images pi
|
|
100
|
-
ON pi.id = pa.image_id
|
|
101
|
-
LEFT JOIN project_classes pc
|
|
102
|
-
ON pc.id = pa.class_id
|
|
103
|
-
WHERE pa.project_id = ?
|
|
104
|
-
AND pi.image_path = ?
|
|
105
|
-
|
|
106
|
-
UNION ALL
|
|
107
|
-
|
|
108
|
-
SELECT
|
|
109
|
-
pa.id,
|
|
110
|
-
pa.project_id,
|
|
111
|
-
pa.user_id,
|
|
112
|
-
pa.coordinates,
|
|
113
|
-
pa.timestamp,
|
|
114
|
-
${FlagStatus.Normal} AS flagged,
|
|
115
|
-
pc.label AS label
|
|
116
|
-
FROM project_annotations pa
|
|
117
|
-
INNER JOIN project_images pi
|
|
118
|
-
ON pi.id = pa.image_id
|
|
119
|
-
LEFT JOIN project_classes pc
|
|
120
|
-
ON pc.id = pa.class_id
|
|
121
|
-
WHERE pa.project_id = ?
|
|
122
|
-
AND pi.image_path = ?
|
|
123
|
-
)
|
|
124
|
-
ORDER BY timestamp DESC, id DESC
|
|
125
|
-
`;
|
|
126
|
-
const GET_PROJECT_IMAGES_SQL = `
|
|
127
|
-
SELECT DISTINCT pi.image_path AS image_path
|
|
128
|
-
FROM project_images pi
|
|
129
|
-
WHERE pi.project_id = ?
|
|
130
|
-
ORDER BY pi.id
|
|
131
|
-
`;
|
|
132
|
-
const GET_FLAGGED_PREDICTIONS_SQL = `
|
|
133
|
-
SELECT
|
|
134
|
-
pa.coordinates,
|
|
135
|
-
pa.flag_type,
|
|
136
|
-
pa.timestamp
|
|
137
|
-
FROM project_flagged_predictions pa
|
|
138
|
-
INNER JOIN project_images pi
|
|
139
|
-
ON pi.id = pa.image_id
|
|
140
|
-
WHERE pa.project_id = ?
|
|
141
|
-
AND pa.image_id = ?
|
|
142
|
-
`;
|
|
143
|
-
if (!ds.queries) ds.queries = {};
|
|
144
|
-
if (!ds.queries.WSImages) ds.queries.WSImages = {};
|
|
145
|
-
ds.queries.WSImages.getWSIAnnotations = async (projectId, filename) => {
|
|
146
|
-
try {
|
|
147
|
-
const stmt = connection.prepare(GET_ANNOTATIONS_SQL);
|
|
148
|
-
const rows = stmt.all(projectId, filename, projectId, filename);
|
|
149
|
-
return rows.map((r) => {
|
|
150
|
-
let coords = [NaN, NaN];
|
|
151
|
-
try {
|
|
152
|
-
const parsed = typeof r.coordinates === "string" ? JSON.parse(r.coordinates) : r.coordinates;
|
|
153
|
-
if (Array.isArray(parsed) && parsed.length >= 2) {
|
|
154
|
-
const x = Number(parsed[0]);
|
|
155
|
-
const y = Number(parsed[1]);
|
|
156
|
-
if (!Number.isNaN(x) && !Number.isNaN(y)) coords = [x, y];
|
|
157
|
-
}
|
|
158
|
-
} catch {
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
zoomCoordinates: coords,
|
|
162
|
-
class: r.label ?? "",
|
|
163
|
-
flag: r.flagged === void 0 ? FlagStatus.Normal : r.flagged,
|
|
164
|
-
timestamp: r.timestamp,
|
|
165
|
-
id: createSelectionID(SelectionPrefixes.Annotation, coords)
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
} catch (error) {
|
|
169
|
-
console.error("Error loading annotations:", error);
|
|
170
|
-
return [];
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
ds.queries.WSImages.getAllWSImages = async (projectId) => {
|
|
174
|
-
try {
|
|
175
|
-
const stmt = connection.prepare(GET_PROJECT_IMAGES_SQL);
|
|
176
|
-
const rows = stmt.all(projectId);
|
|
177
|
-
return (rows || []).map((r) => r.image_path);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error("Error loading project images list:", error);
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
ds.queries.WSImages.getFlaggedPredictions = async (projectId, filename) => {
|
|
184
|
-
const predictionMap = /* @__PURE__ */ new Map();
|
|
185
|
-
const getImageIdSql = `
|
|
186
|
-
SELECT id
|
|
187
|
-
FROM project_images
|
|
188
|
-
WHERE project_id = ?
|
|
189
|
-
AND image_path = ?
|
|
190
|
-
LIMIT 1
|
|
191
|
-
`;
|
|
192
|
-
const getImageStmt = connection.prepare(getImageIdSql);
|
|
193
|
-
const imageRow = getImageStmt.get(projectId, filename);
|
|
194
|
-
if (!imageRow?.id) {
|
|
195
|
-
return predictionMap;
|
|
196
|
-
}
|
|
197
|
-
const imageId = Math.floor(imageRow.id);
|
|
198
|
-
try {
|
|
199
|
-
const stmt = connection.prepare(GET_FLAGGED_PREDICTIONS_SQL);
|
|
200
|
-
const rows = stmt.all(projectId, imageId) || [];
|
|
201
|
-
for (const p of rows) {
|
|
202
|
-
if (p.coordinates && typeof p.coordinates === "string" && typeof p.flag_type === "number")
|
|
203
|
-
predictionMap.set(p.coordinates, { flag: p.flag_type, timestamp: p.timestamp });
|
|
204
|
-
}
|
|
205
|
-
return predictionMap;
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error("Error loading flagged predictions:", error);
|
|
208
|
-
return predictionMap;
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
function validateWSIClassesQuery(ds, connection) {
|
|
213
|
-
if (!ds.queries) ds.queries = {};
|
|
214
|
-
if (!ds.queries.WSImages) ds.queries.WSImages = {};
|
|
215
|
-
const GET_CLASSES_SQL = `
|
|
216
|
-
SELECT id, project_id, label, color, key_shortcut
|
|
217
|
-
FROM project_classes
|
|
218
|
-
WHERE project_id = ?
|
|
219
|
-
ORDER BY id
|
|
220
|
-
`;
|
|
221
|
-
ds.queries.WSImages.getAnnotationClasses = async (projectId) => {
|
|
222
|
-
try {
|
|
223
|
-
const stmt = connection.prepare(GET_CLASSES_SQL);
|
|
224
|
-
return stmt.all(projectId);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error("Error loading project classes:", error);
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
export {
|
|
232
|
-
init,
|
|
233
|
-
validateWSIAnnotationsQuery,
|
|
234
|
-
validateWSIClassesQuery,
|
|
235
|
-
validate_query_getWSIAnnotations,
|
|
236
|
-
validate_query_getWSIClassesQuery
|
|
237
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { checkSelectionType, SelectionPrefixes, FlagStatus } from "#types";
|
|
2
|
-
import { getDbConnection } from "#src/aiHistoDBConnection.ts";
|
|
3
|
-
function init({ genomes }) {
|
|
4
|
-
return async (req, res) => {
|
|
5
|
-
try {
|
|
6
|
-
const query = req.query;
|
|
7
|
-
if (!query.genome) throw new Error(".genome is required for deleteWSITileSelection request.");
|
|
8
|
-
if (!query.dslabel) throw new Error(".dslabel is required for deleteWSITileSelection request.");
|
|
9
|
-
if (!query.tileSelection) throw new Error(".annotation:{} is required for deleteWSITileSelection request.");
|
|
10
|
-
if (!query.projectId) throw new Error(".projectId is required for deleteWSITileSelection request.");
|
|
11
|
-
if (!query.wsimage) throw new Error(".wsimage is required for deleteWSITileSelection request.");
|
|
12
|
-
const g = genomes[query.genome];
|
|
13
|
-
if (!g) throw new Error("invalid genome name");
|
|
14
|
-
const ds = g.datasets[query.dslabel];
|
|
15
|
-
if (!ds) throw new Error("invalid dataset name");
|
|
16
|
-
if (typeof ds.queries?.WSImages?.deleteAnnotation === "function") {
|
|
17
|
-
const result = await ds.queries.WSImages.deleteAnnotation(query, req);
|
|
18
|
-
if (result?.status === "error") {
|
|
19
|
-
return res.status(500).send(result);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
res.status(200).send({ status: `Annotation = ${query.tileSelection.zoomCoordinates} deleted.` });
|
|
23
|
-
} catch (e) {
|
|
24
|
-
console.warn(e);
|
|
25
|
-
res.status(500).send({
|
|
26
|
-
status: "error",
|
|
27
|
-
error: e?.message || String(e)
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function validate_query_deleteWSIAnnotation(ds) {
|
|
33
|
-
if (!ds.queries?.WSImages?.db) return;
|
|
34
|
-
const connection = getDbConnection(ds);
|
|
35
|
-
if (!connection) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
validateQuery(ds, connection);
|
|
39
|
-
}
|
|
40
|
-
function validateQuery(ds, connection) {
|
|
41
|
-
ds.queries.WSImages.deleteAnnotation = async (query, req) => {
|
|
42
|
-
const zoomCoordinates = JSON.stringify(query.tileSelection.zoomCoordinates);
|
|
43
|
-
const { current_user: currentUser } = connection.prepare("SELECT current_user FROM project WHERE id = ?").get(query.projectId);
|
|
44
|
-
if (!await ds.queries?.AIHalAuth?.checkAuthorization(req, "annotate", currentUser)) {
|
|
45
|
-
return {
|
|
46
|
-
status: "error",
|
|
47
|
-
error: "logout"
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const email = req.query.__protected__.clientAuthResult?.email || "";
|
|
51
|
-
if (checkSelectionType(query.tileSelection, SelectionPrefixes.Prediction) && query.tileSelection.flag !== FlagStatus.Normal) {
|
|
52
|
-
try {
|
|
53
|
-
const projectId = query.projectId;
|
|
54
|
-
if (projectId == null) {
|
|
55
|
-
return {
|
|
56
|
-
status: "error",
|
|
57
|
-
error: "Missing required field: projectId"
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const predictionId = query.classID;
|
|
61
|
-
const flagType = query.tileSelection.flag;
|
|
62
|
-
if (predictionId == null) {
|
|
63
|
-
return {
|
|
64
|
-
status: "error",
|
|
65
|
-
error: "Missing prediction id in tileSelection (expected predictionId, prediction.id, or id)."
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const getImageIdSql = `
|
|
69
|
-
SELECT id FROM project_images
|
|
70
|
-
WHERE project_id = ? AND image_path = ? LIMIT 1
|
|
71
|
-
`;
|
|
72
|
-
const imageRow = connection.prepare(getImageIdSql).get(projectId, query.wsimage);
|
|
73
|
-
const imageId = imageRow?.id;
|
|
74
|
-
if (!imageId) {
|
|
75
|
-
return { status: "error", error: "Image not found" };
|
|
76
|
-
}
|
|
77
|
-
connection.prepare(
|
|
78
|
-
`
|
|
79
|
-
DELETE FROM project_flagged_predictions
|
|
80
|
-
WHERE project_id = ? AND coordinates = ?
|
|
81
|
-
AND image_id = (
|
|
82
|
-
SELECT id FROM project_images
|
|
83
|
-
WHERE project_id = ?
|
|
84
|
-
AND image_path = ?
|
|
85
|
-
)
|
|
86
|
-
`
|
|
87
|
-
).run(projectId, zoomCoordinates, projectId, query.wsimage);
|
|
88
|
-
connection.prepare(
|
|
89
|
-
`
|
|
90
|
-
INSERT INTO project_flagged_predictions
|
|
91
|
-
(project_id, user_email, prediction_class_id, coordinates, flag_type, timestamp, image_id)
|
|
92
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
93
|
-
`
|
|
94
|
-
).run(projectId, email, predictionId, zoomCoordinates, flagType, (/* @__PURE__ */ new Date()).toISOString(), imageId);
|
|
95
|
-
return { status: "ok" };
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error("Error inserting flagged prediction:", error);
|
|
98
|
-
return {
|
|
99
|
-
status: "error",
|
|
100
|
-
error: error?.message || "Failed to insert flagged prediction"
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
} else
|
|
104
|
-
try {
|
|
105
|
-
connection.prepare(
|
|
106
|
-
`
|
|
107
|
-
DELETE FROM project_flagged_annotations
|
|
108
|
-
WHERE project_id = ?
|
|
109
|
-
AND coordinates = ?
|
|
110
|
-
AND image_id = (
|
|
111
|
-
SELECT id FROM project_images
|
|
112
|
-
WHERE project_id = ?
|
|
113
|
-
AND image_path = ?
|
|
114
|
-
)
|
|
115
|
-
`
|
|
116
|
-
).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
|
|
117
|
-
connection.prepare(
|
|
118
|
-
`
|
|
119
|
-
DELETE FROM project_annotations
|
|
120
|
-
WHERE project_id = ?
|
|
121
|
-
AND coordinates = ?
|
|
122
|
-
AND image_id = (
|
|
123
|
-
SELECT id FROM project_images
|
|
124
|
-
WHERE project_id = ?
|
|
125
|
-
AND image_path = ?
|
|
126
|
-
)
|
|
127
|
-
`
|
|
128
|
-
).run(query.projectId, zoomCoordinates, query.projectId, query.wsimage);
|
|
129
|
-
return { status: "ok" };
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error("Error deleting annotation:", error);
|
|
132
|
-
return {
|
|
133
|
-
status: "error",
|
|
134
|
-
error: error?.message || "Failed to delete annotation"
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
export {
|
|
140
|
-
init,
|
|
141
|
-
validate_query_deleteWSIAnnotation
|
|
142
|
-
};
|