diff --git a/openflexure_microscope/api/static/main_v1.js b/openflexure_microscope/api/static/main_v1.js index 2962f087..71710062 100644 --- a/openflexure_microscope/api/static/main_v1.js +++ b/openflexure_microscope/api/static/main_v1.js @@ -1,19 +1,3 @@ -/* -TODO: Make delete button work - -TODO: Recordings list -TODO: New recording panel -TODO: Somehow have Python make funky thumbnails for videos? - -TODO: Remember a position and restore that position -TODO: Right click to auto-focus, centered on that region - -TODO: Box to specify device IP -TODO: Dynamically add stream image src from JS (allows streaming from an external IP) -*/ - -var baseURI = "/api/v1" - // Key IDs var pgupKeyID = 33; var pgdnKeyID = 34; @@ -28,71 +12,250 @@ var escKeyID = 27; var keysDown = {}; //Array of keys down -var stageVelocity = 50; -var focusVelocity = 20; - -var requestLock = false; - -// Microscope client-side callibration -//TODO: Move to serverside microscope property -var fovX = 4100; -var fovY = 3146; - -window.onload = function() { - getStagePositions() - updateTextBoxes() - getCaptures() +var clientStageParams = { + "velocityXY": 50, + "velocityZ": 20, + "fov": [0, 0] } +// TODO: Blank out interface except connect menu onload. +window.onload = function() { + // Populate with current URL + document.getElementById('microscopeHostInput').value = window.location.hostname + setHostFromInput() +} + +// Microscope API connection class +class apiConnection { + constructor(hostName, hostPort, apiVersion="v1") { + this.hostName = hostName; + this.hostPort = hostPort; + this.apiVersion = apiVersion; + + this.baseURI = `http://${hostName}:${hostPort}/api/${apiVersion}` + + this.requestLock = false; + + // Used to access 'this' from innter functions + const self = this; + } + + // Identify + identify(callback) { + function identifyCallback(response, status) { + if (status == null) { + console.log("No server found. Aborting.") + } + else { + callback(response, status) + } + } + this.getConfig(identifyCallback) + } + + // Get microscope state + getState(callback) { + this.safeRequest("GET", this.baseURI+"/state", null, callback, false) + } + + // Get microscope configuration + getConfig(callback) { + this.safeRequest("GET", this.baseURI+"/config", null, callback, false) + } + + // New capture + newCapture(filename, temporary, use_video_port, bayer, callback, resizeWidth=null, resizeHeight=null) { + var payload = { + "filename": filename, + "temporary": temporary, + "use_video_port": use_video_port, + "bayer": bayer + } + + if ((resizeWidth) && (resizeHeight)) { + payload.size = { + "width": resizeWidth, + "height": resizeHeight + } + } + + console.log(payload); + this.safeRequest("POST", this.baseURI+"/camera/capture/", payload, callback); + } + + // Delete a capture + deleteCapture(capture_id, callback) { + var r = confirm("Warning! This will delete all copies of this capture from the Raspberry Pi. Click OK to proceed."); + if (r == true) { + this.safeRequest("DELETE", this.baseURI+"/camera/capture/"+capture_id+"/", null, callback, false) + } + } + + // Delete all captures in the session + deleteAllCaptures(callback) { + var r = confirm("Warning! This will delete all copies of all captures from the Raspberry Pi. Click OK to proceed."); + if (r == true) { + this.safeRequest("DELETE", this.baseURI+"/camera/capture/", null, callback, false) + } + } + + // Get the list of captures in the session + getCaptures(callback) { + this.safeRequest("GET", this.baseURI+"/camera/capture", null, callback, false) + } + + // Move stage absolute + setStagePositions(x_abs, y_abs, z_abs, callback, absolute=false) { + // Make a position request + this.safeRequest("POST", this.baseURI+"/stage/position", { "absolute": absolute, "x": x_abs, "y": y_abs, "z": z_abs}, callback) + } + + // Method for making requests + safeRequest(method, url, payload, callback, lock=true, force=false) { + var allowRequest; + + if (force == true) { + allowRequest = true; + } + else { + allowRequest = !(self.requestLock) + } + + if (allowRequest == true) { + var xhr = new XMLHttpRequest(); // new HttpRequest instance + xhr.open(method, url); + + if (lock == true) { // If request is using the lock + self.requestLock = true; // Hold the lock + } + + xhr.onload = function(e) { + if (xhr.readyState === 4) { + if (lock == true) { // If request is using the lock + self.requestLock = false; // Release the lock + } + + if (xhr.status !== 200) { + console.error(xhr.statusText); + } + callback(JSON.parse(xhr.responseText), xhr.status); + } + }; + + xhr.onerror = function(e) { + callback("An error occurred during the transaction.", null); + } + + if (method == "POST" || method == "PUT") { + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + } + + xhr.send(JSON.stringify(payload)); + + } + else { + console.log("Cannot start the following request while the previous requets is pending:") + console.log(method, url, payload) + } + } + +} + +// Declare connection +var api = null + +// Setup methods +function setHostFromInput() { + hostName = document.getElementById('microscopeHostInput').value; + hostPort = document.getElementById('microscopePortInput').value; + + api = new apiConnection(hostName, hostPort) + api.identify(loadInterfaceFromHost) +} + +// Populate the UI from a connected host +function loadInterfaceFromHost(response, status) { + if (status == 200) { + // Populate the preview panel + streamContent = `` + document.getElementById("streambox").innerHTML = streamContent; + + // Update UI with client params + updateClientValues(); + + // Update UI with microscope state + api.getState(updateStateValues) + + // Update UI with microscope config + api.getConfig(updateConfigValues) + + // Update UI with captures + api.getCaptures(constructCaptureList) + + } +} + +function updateClientValues() { + // Microscope velocity + document.getElementById('stageVelocityText').value = clientStageParams.velocityXY; + document.getElementById('stageVelocityInput').value = clientStageParams.velocityXY; + document.getElementById('focusVelocityText').value = clientStageParams.velocityZ; + document.getElementById('focusVelocityInput').value = clientStageParams.velocityZ; +} + +function updateStateValues(response, status){ + if (status == 200) { + updatePositionValues(response); + } +} + +function updateConfigValues(response, status){ + if (status == 200) { + updateFovValues(response); + } +} + +function updatePositionValues(state) { + document.getElementById('x_abs').value = state.stage.position.x; + document.getElementById('y_abs').value = state.stage.position.y; + document.getElementById('z_abs').value = state.stage.position.z; +} + +function updateFovValues(config) { + clientStageParams.fov = config.fov + console.log(clientStageParams.fov) +} // Cosmetic methods + +// Adds/removes a class to a specified div function toggleClassToDiv(div_id, class_name) { document.getElementById(div_id).classList.toggle(class_name) } -// Microscope methods - -function deleteCapture(capture_id) { - function deleteCaptureCallback(response, status) { - console.log(status); - getCaptures(); - } - var r = confirm("Warning! This will delete all copies of this capture from the Raspberry Pi. Click OK to proceed."); - if (r == true) { - safeRequest("DELETE", baseURI+"/camera/capture/"+capture_id+"/", null, deleteCaptureCallback, false) - } +function updateCaptures() { + api.getCaptures(constructCaptureList) } function deleteAllCaptures() { - function deleteAllCapturesCallback(response, status) { - console.log(status); - getCaptures(); - } - var r = confirm("Warning! This will delete all copies of all captures from the Raspberry Pi. Click OK to proceed."); - if (r == true) { - safeRequest("DELETE", baseURI+"/camera/capture/", null, deleteAllCapturesCallback, false) - } + api.deleteAllCaptures(updateCaptures) } -function getCaptures() { - function updateCapturesCallback(response, status) { - console.log(status); - updateCaptureList(response); - } - safeRequest("GET", baseURI+"/camera/capture", null, updateCapturesCallback, false) -} - -function updateCaptureList(response) { +function constructCaptureList(response) { // Clear captures list var capturesNode = document.getElementById("captures"); + while (capturesNode.firstChild) { capturesNode.removeChild(capturesNode.firstChild); } + // Reverse so newest capture appears first + response.reverse() + // For each capture in the response array response.forEach(function(element) { // Store the capture object URI - element_uri = `${baseURI}/camera/capture/${element.metadata.id}` + element_uri = `${api.baseURI}/camera/capture/${element.metadata.id}` // Generate inner HTML from capture object html = ` @@ -104,7 +267,7 @@ function updateCaptureList(response) {
- +
` @@ -118,59 +281,6 @@ function updateCaptureList(response) { } -function updateTextBoxes() { - document.getElementById('stageVelocityText').value = stageVelocity; - document.getElementById('stageVelocityInput').value = stageVelocity; - document.getElementById('focusVelocityText').value = focusVelocity; - document.getElementById('focusVelocityInput').value = focusVelocity; - - document.getElementById('fovXText').value = fovX; - document.getElementById('fovYText').value = fovY; -} - -function updateStagePositions(response) { - document.getElementById('x_abs').value = response.x; - document.getElementById('y_abs').value = response.y; - document.getElementById('z_abs').value = response.z; -} - -function getStagePositions() { - function updatePositionCallback(response, status) { - console.log(status, response); - updateStagePositions(response); - } - safeRequest("GET", baseURI+"/stage/position", null, updatePositionCallback, false) -} - -// Capture methods -function newCapture(filename, temporary, use_video_port, bayer, resizeWidth=null, resizeHeight=null) { - // Make a position request - function newCaptureCallback(response, status) { - if (status != 200) { - alert("Capture failed."); - } - console.log(status, response); - getCaptures(); // Update full list of captures when done - } - - payload = { - "filename": filename, - "temporary": temporary, - "use_video_port": use_video_port, - "bayer": bayer - } - - if ((resizeWidth) && (resizeHeight)) { - payload.size = { - "width": resizeWidth, - "height": resizeHeight - } - } - - console.log(payload); - safeRequest("POST", baseURI+"/camera/capture/", payload, newCaptureCallback); -} - function newCaptureFromInput() { captureFilenameInput = document.getElementById('captureFilenameInput'); captureTemporaryCheck = document.getElementById('captureTemporaryCheck'); @@ -198,13 +308,15 @@ function newCaptureFromInput() { resizeHeight = null; } - newCapture( + // Make a capture request + api.newCapture( filename, captureTemporaryCheck.checked, !(captureFullResolutionCheck.checked), captureBayerCheck.checked, - resizeWidth, - resizeHeight + updateCaptures, + resizeWidth=resizeWidth, + resizeHeight=resizeHeight ); } @@ -218,43 +330,14 @@ function newCaptureResizeToggle(checkbox) { } // Move stage methods -// TODO: Combine setStagePositions and moveStagePositions, with bool argument for 'absolute' - -function setStagePositions(x_abs, y_abs, z_abs) { - // Make a position request - function absMoveCallback(response, status) { - if (status != 200) { - alert("Error moving stage."); - } - console.log(status, response); - updateStagePositions(response); - } - safeRequest("POST", baseURI+"/stage/position", { "absolute": true, "x": x_abs, "y": y_abs, "z": z_abs}, absMoveCallback) -} - -function moveStagePositions(x_rel, y_rel, z_rel) { - // Make a position request - function relMoveCallback(response, status) { - if (status != 200) { - alert("Error moving stage."); - } - console.log(status, response); - updateStagePositions(response); - } - safeRequest("POST", baseURI+"/stage/position", { "absolute": false, "x": x_rel, "y": y_rel, "z": z_rel}, relMoveCallback) -} function setStagePositionsFromInput() { x_abs = Number(document.getElementById('x_abs').value); y_abs = Number(document.getElementById('y_abs').value); z_abs = Number(document.getElementById('z_abs').value); - setStagePositions(x_abs, y_abs, z_abs) -} -// Standard callback for user-controller movement -function keyMoveCallback(response, status) { - console.log(status, response); - updateStagePositions(response); + // Make a position request + api.setStagePositions(x_abs, y_abs, z_abs, updatePositionValues, absolute=true) } // Methods for input events @@ -268,30 +351,31 @@ function clickHotspotImage(event) { xRelative = (0.5*event.target.offsetWidth - xCoordinate)/event.target.offsetWidth; yRelative = (0.5*event.target.offsetHeight - yCoordinate)/event.target.offsetHeight; console.log(xRelative, yRelative) - xSteps = xRelative * fovX; - ySteps = yRelative * fovY; + xSteps = xRelative * clientStageParams.fov[0]; + ySteps = yRelative * clientStageParams.fov[1]; console.log(xSteps, ySteps, 0) + // Make a position request - moveStagePositions(xSteps, ySteps, 0) + api.setStagePositions(xSteps, ySteps, 0, updatePositionValues) } } // Scroll-to-focus window.addEventListener('wheel', function(e) { multiplier = 1/100; - z_delta = e.deltaY * multiplier * focusVelocity; - if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "column middle")) { + z_delta = e.deltaY * multiplier * clientStageParams.velocityZ; + if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "scrolltarget")) { console.log(z_delta); console.log(e.target.parentNode.classList.value); + // Make a position request - safeRequest("POST", baseURI+"/stage/position", { "absolute": false, "z": z_delta}, keyMoveCallback) + api.setStagePositions(0, 0, z_delta, updatePositionValues) } }); // Keyboard to move addEventListener("keydown", function (e) { keysDown[e.keyCode] = true; //Add key to array - console.log(keysDown) // If not currently in an input box if (!(e.target instanceof HTMLInputElement)) { @@ -304,26 +388,26 @@ addEventListener("keydown", function (e) { y_rel = 0; z_rel = 0; if (leftKeyID in keysDown) { - x_rel = x_rel + stageVelocity; + x_rel = x_rel + clientStageParams.velocityXY; } if (rightKeyID in keysDown) { - x_rel = x_rel - stageVelocity; + x_rel = x_rel - clientStageParams.velocityXY; } if (upKeyID in keysDown) { - y_rel = y_rel + stageVelocity; + y_rel = y_rel + clientStageParams.velocityXY; } if (downKeyID in keysDown) { - y_rel = y_rel - stageVelocity; + y_rel = y_rel - clientStageParams.velocityXY; } if (pgupKeyID in keysDown) { - z_rel = z_rel - focusVelocity; + z_rel = z_rel - clientStageParams.velocityZ; } if (pgdnKeyID in keysDown) { - z_rel = z_rel + focusVelocity; + z_rel = z_rel + clientStageParams.velocityZ; } // Make a position request - moveStagePositions(x_rel, y_rel, z_rel) + api.setStagePositions(x_rel, y_rel, z_rel, updatePositionValues) } } @@ -333,44 +417,4 @@ addEventListener("keyup", function (e) { delete keysDown[e.keyCode]; //Remove key from array }, false); -// Methods for making requests -function safeRequest(method, url, payload, callback, lock=true, force=false) { - if (force == true) { - allowRequest = true; - } - else { - allowRequest = !(requestLock); - } - if (allowRequest == true) { - var xhr = new XMLHttpRequest(); // new HttpRequest instance - xhr.open(method, url); - if (lock == true) { // If request is using the lock - requestLock = true; // Hold the lock - } - - xhr.onload = function (e) { - if (xhr.readyState === 4) { - if (lock == true) { // If request is using the lock - requestLock = false; // Release the lock - } - - if (xhr.status !== 200) { - console.error(xhr.statusText); - } - callback(JSON.parse(xhr.responseText), xhr.status); - } - }; - - if (method == "POST" || method == "PUT") { - xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - } - - xhr.send(JSON.stringify(payload)); - - } - else { - console.log("Cannot start the following request while the previous requets is pending:") - console.log(method, url, payload) - } -} diff --git a/openflexure_microscope/api/templates/index_v1.html b/openflexure_microscope/api/templates/index_v1.html index d93702d6..7cfadefb 100644 --- a/openflexure_microscope/api/templates/index_v1.html +++ b/openflexure_microscope/api/templates/index_v1.html @@ -8,11 +8,15 @@ +

Settings

+ Host:
+ Port:
+

Position

@@ -27,19 +31,6 @@

Doubleclick to position:
-

-
FOV width:
-
- -
-
- -
-
FOV height:
-
- -
-

@@ -56,14 +47,14 @@
x-y:
- +
z:
- +
@@ -84,7 +75,7 @@
- +
No active stream
@@ -101,7 +92,7 @@

Captures

- +