From 6b22e32b590aab8cc5266aabb812572bf6104d90 Mon Sep 17 00:00:00 2001 From: Leander Lauenburg Date: Mon, 24 Mar 2025 09:37:26 +0100 Subject: [PATCH 01/11] update draw module with wheel scroll and icons --- .gitignore | 1 + synanno/routes/manual_annotate.py | 2 +- synanno/static/annotation_module.js | 7 + synanno/static/css/style.css | 44 ++- synanno/static/draw.js | 116 +++--- synanno/static/draw_module.js | 481 +++++++++--------------- synanno/templates/annotation_neuro.html | 13 +- synanno/templates/draw_modal.html | 89 +++-- synanno/templates/templatebase.html | 3 + 9 files changed, 351 insertions(+), 405 deletions(-) diff --git a/.gitignore b/.gitignore index c08a7b3..663d41e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,5 +53,6 @@ datasets/ flask_session synAnno.json secrets/ +tmp/ .vscode/ diff --git a/synanno/routes/manual_annotate.py b/synanno/routes/manual_annotate.py index ea80c27..ea40b26 100644 --- a/synanno/routes/manual_annotate.py +++ b/synanno/routes/manual_annotate.py @@ -79,7 +79,7 @@ def save_canvas() -> dict: image = decode_image(request.form["imageBase64"]) page = int(request.form["page"]) index = int(request.form["data_id"]) - viewed_instance_slice = int(request.form["viewed_instance_slice"]) + viewed_instance_slice = int(request.form["viewedInstanceSlice"]) canvas_type = str(request.form["canvas_type"]) crop_axes = ( diff --git a/synanno/static/annotation_module.js b/synanno/static/annotation_module.js index 5e869a4..889219f 100755 --- a/synanno/static/annotation_module.js +++ b/synanno/static/annotation_module.js @@ -120,6 +120,13 @@ $(document).ready(function () { isModalScrollingLocked = true; const newSlice = currentSlice + (event.originalEvent.deltaY > 0 ? 1 : -1); + + // Restrict scrolling beyond available slices + if (newSlice < dataJson.Min_Slice || newSlice > dataJson.Max_Slice) { + isModalScrollingLocked = false; + return; + } + try { const exists = await $.get(dataJson.Error_Description === "False Negative" ? `/source_img_exists/${dataId}/${newSlice}` : diff --git a/synanno/static/css/style.css b/synanno/static/css/style.css index 010bd66..ac96909 100755 --- a/synanno/static/css/style.css +++ b/synanno/static/css/style.css @@ -96,6 +96,9 @@ margin: auto; width: 384px; height: 384px; + background-color: #fff; + border-radius: 12px; + overflow: hidden; } .insideWrapper { @@ -108,8 +111,8 @@ width: 100%; height: 100%; position: absolute; - top: 0px; - left: 0px; + top: 0; + left: 0; pointer-events: none; z-index: 0; } @@ -122,6 +125,43 @@ left: 0; } +/* Clean, modern visual style */ +.modal-content { + background: #ffffff; + border-radius: 1rem; + padding: 1rem; + border: none; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.modal-dialog { + border: none !important; + box-shadow: none !important; +} + +.modal-header, +.card-header { + background: linear-gradient(to right, #f8f9fa, #ffffff); + border-bottom: none !important; + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + box-shadow: none; +} + +.btn { + transition: all 0.2s ease-in-out; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.modal-footer { + padding: 0rem 1rem; + max-height: 40px; + border-top: none; +} /* css for the open data form */ .syn-id-container { display: flex; diff --git a/synanno/static/draw.js b/synanno/static/draw.js index ea43c99..826d60a 100755 --- a/synanno/static/draw.js +++ b/synanno/static/draw.js @@ -14,6 +14,7 @@ $(document).ready(() => { let points = [], pointsQBez = []; const thickness = 15; const pink = "rgba(255, 0, 255, 0.7)"; + const turquoise = "rgba(21, 229, 239, 0.92)"; let page, data_id, label; let pre_CRD = false, post_CRD = false; let x_syn_crd = null, y_syn_crd = null; @@ -96,10 +97,10 @@ $(document).ready(() => { $('[id^="drawButton-"]').click(async function () { [page, data_id, label] = $(this).attr("id").replace(/drawButton-/, "").split("-"); - $("#canvasButtonDrawMask").text("Draw Mask").prop("disabled", false); - $("#canvasButtonPreCRD").text("Pre-Synaptic CRD").prop("disabled", false); - $("#canvasButtonPostCRD").text("Post-Synaptic CRD").prop("disabled", false); - $("#canvasButtonAuto, #rangeSlices").prop("disabled", false); + $("#canvasButtonDrawMask").html('') + .attr("title", "Draw Mask").prop("disabled", false); + $("#canvasButtonPreCRD").prop("disabled", false); + $("#canvasButtonPostCRD").prop("disabled", false); $("#canvasButtonFill, #canvasButtonRevise, #canvasButtonSave").prop("disabled", true); ctx_curve.restore(); clear_canvas(ctx_curve, canvas_curve); @@ -316,7 +317,9 @@ $(document).ready(() => { ctx_curve.restore(); if ($("canvas.curveCanvas").hasClass("d-none")) { $("#imgDetails-EM-GT-curve").addClass("d-none"); - $("#canvasButtonDrawMask").text("Reset"); + $("#canvasButtonDrawMask") + .html('') + .attr("title", "Reset"); $("canvas.curveCanvas").removeClass("d-none"); rect_curve = $("#imgDetails-EM")[0].getBoundingClientRect(); $("canvas.curveCanvas")[0].width = rect_curve.width; @@ -326,11 +329,13 @@ $(document).ready(() => { $("canvas.circleCanvasPre").css("z-index", 1); split_mask = false; draw_mask = true; - $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto, #rangeSlices").prop("disabled", true); + $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto").prop("disabled", true); } else { $("canvas.curveCanvas").addClass("d-none"); - $("#canvasButtonDrawMask").text("Draw Mask"); - $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto, #rangeSlices").prop("disabled", false); + $("#canvasButtonDrawMask") + .html('') + .attr("title", "Draw Mask"); + $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto").prop("disabled", false); $("#canvasButtonFill, #canvasButtonRevise, #canvasButtonSave").prop("disabled", true); points = []; pointsQBez = []; @@ -366,7 +371,7 @@ $(document).ready(() => { async function save_canvas(canvas, canvas_type) { const dataURL = canvas.toDataURL(); - const viewed_instance_slice = parseInt($("#rangeSlices").data("viewed_instance_slice")); + const viewedInstanceSlice = parseInt($("#drawModal").data("viewed-instance-slice")); try { const response = await $.ajax({ @@ -376,7 +381,7 @@ $(document).ready(() => { imageBase64: dataURL, data_id: data_id, page: page, - viewed_instance_slice: viewed_instance_slice, + viewedInstanceSlice: viewedInstanceSlice, canvas_type: canvas_type, }, }); @@ -386,17 +391,17 @@ $(document).ready(() => { if (canvas_type === 'curve') { $.ajax({ - url: "/get_curve_image/" + data_id + "/" + viewed_instance_slice, + url: "/get_curve_image/" + data_id + "/" + viewedInstanceSlice, type: 'HEAD', success: function (data, textStatus, xhr) { if (xhr.status === 200) { // Only execute if status is 200 (image exists) - if (viewed_instance_slice === parseInt(data_json.Middle_Slice, 10)) { - $(new Image()).attr("src", "/get_curve_image/" + data_id + "/" + viewed_instance_slice).on("load", function () { + if (viewedInstanceSlice === parseInt(data_json.Middle_Slice, 10)) { + $(new Image()).attr("src", "/get_curve_image/" + data_id + "/" + viewedInstanceSlice).on("load", function () { $(canvas_target_image).attr("src", this.src); }); $(canvas_target_image).removeClass('d-none'); } - $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto, #rangeSlices").prop("disabled", false); + $("#canvasButtonPreCRD, #canvasButtonPostCRD, #canvasButtonAuto").prop("disabled", false); $("#canvasButtonSave, #canvasButtonFill, #canvasButtonRevise").prop("disabled", true); } else if (xhr.status === 204) { @@ -414,7 +419,7 @@ $(document).ready(() => { data: { x: x_syn_crd, y: y_syn_crd, - z: viewed_instance_slice, + z: viewedInstanceSlice, data_id: data_id, page: page, id: id, @@ -423,14 +428,14 @@ $(document).ready(() => { if (canvas_type === 'circlePre') { $.ajax({ - url: "/get_circle_pre_image/" + data_id + "/" + viewed_instance_slice, + url: "/get_circle_pre_image/" + data_id + "/" + viewedInstanceSlice, type: "HEAD", success: function (data, textStatus, xhr) { if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()).attr("src", "/get_circle_pre_image/" + data_id + "/" + viewed_instance_slice).on("load", function () { + $(new Image()).attr("src", "/get_circle_pre_image/" + data_id + "/" + viewedInstanceSlice).on("load", function () { $(canvas_target_image).attr("src", this.src); }); - if (viewed_instance_slice === parseInt(data_json.Middle_Slice, 10)) { + if (viewedInstanceSlice === parseInt(data_json.Middle_Slice, 10)) { $(canvas_target_image).removeClass('d-none'); } else { $(canvas_target_image).addClass('d-none'); @@ -444,14 +449,14 @@ $(document).ready(() => { if (canvas_type === 'circlePost') { $.ajax({ - url: "/get_circle_post_image/" + data_id + "/" + viewed_instance_slice, + url: "/get_circle_post_image/" + data_id + "/" + viewedInstanceSlice, type: "HEAD", success: function (data, textStatus, xhr) { if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()).attr("src", "/get_circle_post_image/" + data_id + "/" + viewed_instance_slice).on("load", function () { + $(new Image()).attr("src", "/get_circle_post_image/" + data_id + "/" + viewedInstanceSlice).on("load", function () { $(canvas_target_image).attr("src", this.src); }); - if (viewed_instance_slice === parseInt(data_json.Middle_Slice, 10)) { + if (viewedInstanceSlice === parseInt(data_json.Middle_Slice, 10)) { $(canvas_target_image).removeClass('d-none'); } else { $(canvas_target_image).addClass('d-none'); @@ -466,7 +471,7 @@ $(document).ready(() => { const curve_src = $(`#img-target-curve${base}`).attr('src'); const custom_mask = curve_src.includes("curve_image"); - if (!custom_mask && viewed_instance_slice === parseInt(data_json.Middle_Slice, 10)) { + if (!custom_mask && viewedInstanceSlice === parseInt(data_json.Middle_Slice, 10)) { $(`#img-target-curve${base}`).addClass('d-none'); } @@ -482,7 +487,7 @@ $(document).ready(() => { const pos = getXY(canvas_curve, event, rect_curve); const x = pos.x; const y = pos.y; - ctx_curve.strokeStyle = "#000"; + ctx_curve.strokeStyle = pink; ctx_curve.beginPath(); ctx_curve.ellipse(x, y, thickness, Math.floor(thickness / 2), 0, 0, Math.PI * 2); ctx_curve.stroke(); @@ -494,28 +499,37 @@ $(document).ready(() => { $("canvas.curveCanvas").on("click", (e) => { if (draw_mask) { clear_canvas(ctx_curve, canvas_curve); - ctx_curve.beginPath(); const pos = getXY(canvas_curve, e, rect_curve); const x = pos.x; const y = pos.y; points.push({ x, y }); + if (points.length > 2) { draw_quad_line(points, 3); $("#canvasButtonFill").prop("disabled", false); } else if (points.length > 1) { ctx_curve.lineWidth = 3; + ctx_curve.strokeStyle = pink; + + // Draw line + ctx_curve.beginPath(); ctx_curve.moveTo(points[0].x, points[0].y); - ctx_curve.fillStyle = 'red'; - ctx_curve.fillRect(points[0].x, points[0].y, 4, 4); - ctx_curve.fillRect(points[1].x, points[1].y, 4, 4); - ctx_curve.fillStyle = 'black'; - ctx_curve.lineTo(points[0].x, points[0].y); ctx_curve.lineTo(points[1].x, points[1].y); ctx_curve.stroke(); + + // Draw turquoise points as squares (or change to circles if you prefer) + ctx_curve.fillStyle = turquoise; + ctx_curve.beginPath(); + ctx_curve.arc(points[0].x, points[0].y, 4, 0, Math.PI * 2); + ctx_curve.fill(); + ctx_curve.beginPath(); + ctx_curve.arc(points[1].x, points[1].y, 4, 0, Math.PI * 2); + ctx_curve.fill(); } } }); + function clear_canvas(ctx, canvas) { ctx.beginPath(); ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -565,40 +579,42 @@ $(document).ready(() => { ctx_curve.rect(0, 0, canvas_curve.width, canvas_curve.height); ctx_curve.fill(); ctx_curve.restore(); + + // Deactivate buttons after spline is filled + $("#canvasButtonFill, #canvasButtonRevise").prop("disabled", true); } else { console.log("A mask needs at least 3 points"); } } function draw_quad_line(points, lineWidth = 3, draw_points = true) { - ctx_curve.beginPath(); - ctx_curve.strokeStyle = 'black'; + ctx_curve.strokeStyle = pink; ctx_curve.lineWidth = lineWidth; + + ctx_curve.beginPath(); ctx_curve.moveTo(points[0].x, points[0].y); - if (draw_points) { - ctx_curve.fillStyle = 'red'; - ctx_curve.fillRect(points[0].x, points[0].y, 4, 4); - if (points.length < 4) { - ctx_curve.fillRect(points[0].x, points[0].y, 4, 4); - ctx_curve.fillRect(points[1].x, points[1].y, 4, 4); - } else { - ctx_curve.fillRect(points[0].x, points[0].y, 4, 4); - } - } - for (i = 1; i < points.length - 2; i++) { - var xend = (points[i].x + points[i + 1].x) / 2; - var yend = (points[i].y + points[i + 1].y) / 2; + + for (let i = 1; i < points.length - 2; i++) { + const xend = (points[i].x + points[i + 1].x) / 2; + const yend = (points[i].y + points[i + 1].y) / 2; ctx_curve.quadraticCurveTo(points[i].x, points[i].y, xend, yend); - if (draw_points) { - ctx_curve.fillRect(points[i].x, points[i].y, 4, 4); - ctx_curve.fillRect(points[i + 1].x, points[i + 1].y, 4, 4); - } } + + const i = points.length - 2; ctx_curve.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y); + + // 🔥 Draw the line first + ctx_curve.stroke(); + + // 🎯 Now draw the point markers *after* the line if (draw_points) { - ctx_curve.fillRect(points[i + 1].x, points[i + 1].y, 4, 4); + ctx_curve.fillStyle = turquoise; + for (let p of points) { + ctx_curve.beginPath(); + ctx_curve.arc(p.x, p.y, 4, 0, Math.PI * 2); + ctx_curve.fill(); + } } - ctx_curve.stroke(); } function _getQBezierValue(t, p1, p2, p3) { diff --git a/synanno/static/draw_module.js b/synanno/static/draw_module.js index 262eddf..1cc9c5a 100755 --- a/synanno/static/draw_module.js +++ b/synanno/static/draw_module.js @@ -1,345 +1,223 @@ -$(document).ready(function () { - +$(document).ready(() => { const currentPage = parseInt($("script[src*='draw_module.js']").data("current-page")) || -1; - const current_view = $("script[src*='draw_module.js']").data("current-view") || "draw"; + const currentView = $("script[src*='draw_module.js']").data("current-view") || "draw"; - $('[id^="drawButton-"]').click(async function () { - var [page, data_id, label] = $(this) - .attr("id") - .replace(/drawButton-/, "") - .split("-"); - - // we are currently in drawing mode - var mode = "draw"; - - // we require the information about the whole instance - var load = "full"; - - let req_data = $.ajax({ - url: "/get_instance", - type: "POST", - data: { - mode: mode, - load: load, - data_id: data_id, - page: page, + // active mouse tip title depictions + const tooltipTriggerList = $('[title]').toArray(); + tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el)); + + let page = 0; + let dataId = 0; + let label = ""; + let currentSlice = 0; + let dataJson = null; + + const $drawModal = $("#drawModal"); + const $imgSource = $("#imgDetails-EM"); + const $imgTarget = $("#imgDetails-EM-GT-curve"); + const $imgPreCircle = $("#imgDetails-EM-GT-circlePre"); + const $imgPostCircle = $("#imgDetails-EM-GT-circlePost"); + + + const loadImage = (url, $element) => { + $.ajax({ + url: url, + type: "HEAD", + success: function (data, textStatus, xhr) { + if (xhr.status === 200) { + // Image exists + $(new Image()) + .attr("src", url) + .on("load", function () { + $element.attr("src", this.src).removeClass("d-none"); + }); + } else if (xhr.status === 204) { + // No content + console.info("Status 204: No content found for image:", url); + $element.addClass("d-none"); + } }, + error: function (xhr) { + console.error("Error loading image:", url, "Status:", xhr.status); + $element.addClass("d-none"); + } }); + }; - let cz0 = 0; - let cy0 = 0; - let cx0 = 0; - // set the base image to the center slice - await req_data.done(function (data) { - let data_json = JSON.parse(data.data); - $("#imgDetails-EM").addClass(label.toLowerCase()); - $("#imgDetails-EM").attr("src", "/get_source_image/" + data_id + "/" + data_json.Middle_Slice); + const updateImages = async (dataId, slice) => { + try { + // Update curve image + await loadImage(`/get_curve_image/${dataId}/${slice}`, $imgTarget); - if (mode === "draw") { - $.ajax({ - url: "/get_curve_image/" + data_id + "/" + data_json.Middle_Slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_curve_image/" + data_id + "/" + data_json.Middle_Slice) - .load(function () { - $("#imgDetails-EM-GT-curve").attr("src", this.src); - }); - $("#imgDetails-EM-GT-curve").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if image does not exist) - console.log("Curve image does not exist"); - - $.ajax({ - url: "/get_auto_curve_image/" + data_id + "/" + data_json.Middle_Slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_auto_curve_image/" + data_id + "/" + data_json.Middle_Slice) - .load(function () { - $("#imgDetails-EM-GT-curve").attr("src", this.src); - }); - $("#imgDetails-EM-GT-curve").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if image does not exist) - console.log("Curve image does not exist"); - $("#imgDetails-EM-GT-curve").addClass("d-none"); - } - }, - }); - } - }, - }); + // Update auto curve image if curve image does not exist + if ($imgTarget.hasClass("d-none")) { + await loadImage(`/get_auto_curve_image/${dataId}/${slice}`, $imgTarget); + } - $.ajax({ - url: "/get_circle_pre_image/" + data_id + "/" + data_json.Middle_Slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_circle_pre_image/" + data_id + "/" + data_json.Middle_Slice) - .on("load", function () { - $("#imgDetails-EM-GT-circlePre").attr("src", this.src); - }); - $("#imgDetails-EM-GT-circlePre").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - console.log("Circle Pre image does not exist"); - $("#imgDetails-EM-GT-circlePre").addClass("d-none"); - } - }, - error: function () { - $("#imgDetails-EM-GT-circlePre").addClass("d-none"); - }, - }); + // Update pre-synaptic marker + await loadImage(`/get_circle_pre_image/${dataId}/${slice}`, $imgPreCircle); - $.ajax({ - url: "/get_circle_post_image/" + data_id + "/" + data_json.Middle_Slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_circle_post_image/" + data_id + "/" + data_json.Middle_Slice) - .load(function () { - $("#imgDetails-EM-GT-circlePost").attr("src", this.src); - }); - $("#imgDetails-EM-GT-circlePost").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - console.log("Circle Post image does not exist"); - $("#imgDetails-EM-GT-circlePost").addClass("d-none"); - } - } - }); - } + // Update post-synaptic marker + await loadImage(`/get_circle_post_image/${dataId}/${slice}`, $imgPostCircle); + } catch (error) { + console.error("Error updating images:", error); + } + }; + + $('[id^="drawButton-"]').click(async function () { + [page, dataId, label] = $(this).attr("id").replace(/drawButton-/, "").split("-"); - // update the range slider - $("#rangeSlices").attr("min", data.range_min); - $("#rangeSlices").attr("max", data.range_min + data.number_of_slices - 1); - $("#rangeSlices").val(data.halflen); - $("#rangeSlices").attr("data_id", data_id); - $("#rangeSlices").attr("page", page); + const mode = "draw"; + const load = "full"; // Load full instance data - $("#minSlice").html(0); - $("#maxSlice").html(data.number_of_slices - 1); + try { + const reqData = await $.ajax({ + url: "/get_instance", + type: "POST", + data: { mode, load, data_id: dataId, page }, + }); - cz0 = data_json.cz0; - cy0 = data_json.cy0; - cx0 = data_json.cx0; + dataJson = JSON.parse(reqData.data); + currentSlice = dataJson.Middle_Slice; - $("#rangeSlices").data("viewed_instance_slice", data_json.Middle_Slice); - }); + // Set viewed instance slice + $drawModal.data("viewed-instance-slice", currentSlice); - // retrieve the updated NG link - let req_ng = $.ajax({ - url: "/neuro", - type: "POST", - // we set mode to 'annotate' as we would like to set the focus on the particular instance - data: { cz0: cz0, cy0: cy0, cx0: cx0, mode: "annotate" }, - }); + $imgSource.addClass(label.toLowerCase()).attr( + "src", + `/get_source_image/${dataId}/${dataJson.Middle_Slice}` + ); - req_ng.done(function (data) { - let ng_link = data.ng_link; - $("#ng-iframe-draw").attr("src", ng_link); - }); + if (mode === "draw") { + await updateImages(dataId, dataJson.Middle_Slice); + } + + const { cz0, cy0, cx0 } = dataJson; + const reqNg = await $.ajax({ + url: "/neuro", + type: "POST", + data: { cz0, cy0, cx0, mode: "annotate" }, + }); + + $("#ng-iframe-draw").attr("src", reqNg.ng_link); + } catch (error) { + console.error("Error loading instance:", error); + } }); - // with in the modal view retrieve the instance specific data - // to switch between the slices - $("#rangeSlices").on("input", function () { - // set the visibility of the canvases to hidden - // this will trigger the deletion of the canvas - $("canvas.curveCanvas").addClass("d-none"); - $("canvas.circleCanvasPre").addClass("d-none"); - $("canvas.circleCanvasPost").addClass("d-none"); - - // reset all buttons - $("#canvasButtonPreCRD").text("Pre-Synaptic CRD"); - $("#canvasButtonPostCRD").text("Post-Synaptic CRD"); - $("#canvasButtonPreCRD").prop("disabled", false); - $("#canvasButtonPostCRD").prop("disabled", false); - - // reset the draw mask button - $("#canvasButtonDrawMask").text("Draw Mask"); - $("#canvasButtonDrawMask").prop("disabled", false); + let isModalScrollingLocked = false; + let modalScrollDelta = 0; + const MODAL_SCROLL_THRESHOLD = 65; - // disable all options except the activate canvas button - $("#canvasButtonFill").prop("disabled", true); - $("#canvasButtonRevise").prop("disabled", true); - $("#canvasButtonSave").prop("disabled", true); - - // get the current slice slice index - let viewed_instance_slice = $(this).val(); - // update the appropriate attribute to be used by the draw.js script - $(this).data("viewed_instance_slice", viewed_instance_slice); - - // the instance identifiers - var data_id = $(this).attr("data_id"); - var page = $(this).attr("page"); - - // we are currently in drawing mode - var mode = "draw"; - - // we only require the path to load a single slice and the corresponding GT - var load = "single"; - - // retrieve the information from the backend - let req = $.ajax({ - url: "/get_instance", - type: "POST", - data: { - mode: mode, - load: load, - data_id: data_id, - page: page, - viewed_instance_slice: viewed_instance_slice, - }, - }); + $drawModal.on("wheel", async (event) => { + event.preventDefault(); + if (isModalScrollingLocked) return; - // update the slice and GT that is depicted - req.done(function (data) { - $("#imgDetails-EM").attr("src", "/get_source_image/" + data_id + "/" + viewed_instance_slice); + modalScrollDelta += event.originalEvent.deltaY; - if (mode === "draw") { - $.ajax({ - url: "/get_auto_curve_image/" + data_id + "/" + viewed_instance_slice, - type: 'HEAD', - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_auto_curve_image/" + data_id + "/" + viewed_instance_slice) - .load(function () { - $("#imgDetails-EM-GT-curve").attr("src", this.src); - }); - $("#imgDetails-EM-GT-curve").removeClass('d-none'); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - $.ajax({ - url: "/get_curve_image/" + data_id + "/" + viewed_instance_slice, - type: 'HEAD', - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_curve_image/" + data_id + "/" + viewed_instance_slice) - .load(function () { - $("#imgDetails-EM-GT-curve").attr("src", this.src); - }); - $("#imgDetails-EM-GT-curve").removeClass('d-none'); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - console.log("Curve image does not exist"); - $("#imgDetails-EM-GT-curve").addClass('d-none'); - } - } - }); - } - } - }); + if (Math.abs(modalScrollDelta) < MODAL_SCROLL_THRESHOLD) return; - $.ajax({ - url: "/get_circle_pre_image/" + data_id + "/" + viewed_instance_slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_circle_pre_image/" + data_id + "/" + viewed_instance_slice) - .load(function () { - $("#imgDetails-EM-GT-circlePre").attr("src", this.src); - }); - $("#imgDetails-EM-GT-circlePre").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - console.log("Circle Pre image does not exist"); - $("#imgDetails-EM-GT-circlePre").addClass("d-none"); - } - }, - }); + modalScrollDelta = 0; + isModalScrollingLocked = true; - $.ajax({ - url: "/get_circle_post_image/" + data_id + "/" + viewed_instance_slice, - type: "HEAD", - success: function (data, textStatus, xhr) { - if (xhr.status === 200) { // Only execute if status is 200 (image exists) - $(new Image()) - .attr("src", "/get_circle_post_image/" + data_id + "/" + viewed_instance_slice) - .load(function () { - $("#imgDetails-EM-GT-circlePost").attr("src", this.src); - }); - $("#imgDetails-EM-GT-circlePost").removeClass("d-none"); - } - else if (xhr.status === 204) { // Only execute if status is 404 (image does not exist) - console.log("Circle Post image does not exist"); - $("#imgDetails-EM-GT-circlePost").addClass("d-none"); - } - }, - }); + $("canvas.curveCanvas, canvas.circleCanvasPre, canvas.circleCanvasPost").addClass("d-none"); + $("#canvasButtonPreCRD, #canvasButtonPostCRD").prop("disabled", false); + $("#canvasButtonFill, #canvasButtonRevise, #canvasButtonSave").prop("disabled", true); + // return the draw mask button to the pencil icon + $("#canvasButtonDrawMask").prop("disabled", false); + $("#canvasButtonDrawMask") + .html('') + .attr("title", "Draw Mask"); + const newSlice = currentSlice + (event.originalEvent.deltaY > 0 ? 1 : -1); + + // Restrict scrolling beyond available slices + if (newSlice < dataJson.Min_Slice || newSlice > dataJson.Max_Slice) { + isModalScrollingLocked = false; + return; + } + + try { + const req = await $.ajax({ + url: "/get_instance", + type: "POST", + data: { + mode: "draw", + load: "single", + data_id: dataId, + page, + viewedInstanceSlice: newSlice, + }, + }); + + const exists = await $.get(`/source_img_exists/${dataId}/${newSlice}`); + if (exists) { + $imgSource.attr("src", `/get_source_image/${dataId}/${newSlice}`); + await updateImages(dataId, newSlice); + currentSlice = newSlice; + // Ensure viewedInstanceSlice is always set correctly + $drawModal.data("viewed-instance-slice", newSlice); } - }); - }); - - $("#add_new_instance").click(async function () { - // open a new Neuroglancer view - let req_ng = $.ajax({ - url: "/neuro", - type: "POST", - data: { cz0: 0, cy0: 0, cx0: 0, mode: "draw" }, - }); - req_ng.done(function (data) { - let ng_link = data.ng_link; - $("#ng-iframe-draw").attr("src", ng_link); - }); + } catch (error) { + console.error("Error loading slice:", error); + } finally { + isModalScrollingLocked = false; + } + }); - $("#review_bbox").show(); - $("#back_to_2d").hide(); + $("#add_new_instance").click(async () => { + try { + const reqNg = await $.ajax({ + url: "/neuro", + type: "POST", + data: { cz0: 0, cy0: 0, cx0: 0, mode: "draw" }, + }); + + $("#ng-iframe-draw").attr("src", reqNg.ng_link); + $("#review_bbox").show(); + $("#back_to_2d").hide(); + } catch (error) { + console.error("Error adding new instance:", error); + } }); - // toggle the functionality of the NG module based on the current use-case: select slice for drawing - $("#ng-link-draw").on("click", function () { + $("#ng-link-draw").click(() => { $("#review_bbox").hide(); $("#back_to_2d").show(); }); - // ensure that the canvas is depicted when returning to the 2D view - $("#back_to_2d").on("click", function () { - $("canvas.curveCanvas").removeClass("d-none"); // change the visibility of the canvas - // TODO if I check the ng before drawing any thing we will see the picture logo instead of the canvas - // Have to also check this logic for the circle canvas + $("#back_to_2d").click(() => { + $("canvas.curveCanvas").removeClass("d-none"); }); - $("#review_bbox").click(async function (e) { - // retrieve the bb information from the backend - $.ajax({ - url: "/ng_bbox_fn", - type: "POST", - data: { z1: 0, z2: 0, my: 0, mx: 0 }, - }).done(function (data) { + $("#review_bbox").click(async () => { + try { + const data = await $.ajax({ + url: "/ng_bbox_fn", + type: "POST", + data: { z1: 0, z2: 0, my: 0, mx: 0 }, + }); + $("#m_x").val(data.mx); $("#m_y").val(data.my); - $("#d_z1").val(data.z1); $("#d_z2").val(data.z2); - }); + } catch (error) { + console.error("Error reviewing bbox:", error); + } }); - // add event listener if current view is draw - if (current_view === "draw") { - $("#save_bbox").click(async function () { - - // show loading-bar - $('#loading-bar').css('display', 'flex'); + if (currentView === "draw") { + $("#save_bbox").click(async () => { + $("#loading-bar").css("display", "flex"); try { - // update the bb information with the manual corrections and pass them to the backend - // trigger the processing/save to the pandas df in the backend await $.ajax({ url: "/ng_bbox_fn_save", type: "POST", data: { - currentPage: currentPage, + currentPage, z1: $("#d_z1").val(), z2: $("#d_z2").val(), my: $("#m_y").val(), @@ -347,18 +225,13 @@ $(document).ready(function () { }, }); - // hide modules $("#drawModalFNSave, #drawModalFN").modal("hide"); - - // refresh page location.reload(); } catch (error) { - console.error('Error saving bbox:', error); + console.error("Error saving bbox:", error); } finally { - // Hide the loading bar after the operation completes - $('#loading-bar').css('display', 'none'); + $("#loading-bar").css("display", "none"); } }); } - }); diff --git a/synanno/templates/annotation_neuro.html b/synanno/templates/annotation_neuro.html index dff68c5..7a5e721 100644 --- a/synanno/templates/annotation_neuro.html +++ b/synanno/templates/annotation_neuro.html @@ -6,8 +6,17 @@ tabindex="-1" inert > -