Commit 95e10b14 authored by Joel Collins's avatar Joel Collins
Browse files

Initial commit

parents
Loading
Loading
Loading
Loading

index.html

0 → 100644
+117 −0
Original line number Diff line number Diff line
<html>
  <head>
    <title>OpenFlexure Microscope API Demo</title>

    <link rel= "stylesheet" type= "text/css" href="./static/style.css">
    <script type="text/javascript" src="./static/main.js"></script>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui, maximum-scale=1.0, user-scalable=0">

  </head>
  <body>

    <div class="row">
      <div class="column left" id="left-sb">
        <h2>Settings</h2>
        Host: <input type="text" id="microscopeHostInput" value="192.168.1.126"><br>
        Port: <input type="number" id="microscopePortInput" value=5000><br>
        <button type="button" onclick="setHostFromInput();">Connect</button>

        <h3>Position</h3>
        <p>
          x: <input type="number" id="x_abs"><br>
          y: <input type="number" id="y_abs"><br>
          z: <input type="number" id="z_abs"><br>
        </p>
        <p>
          <button type="button" onclick="setStagePositionsFromInput();">GO</button>
          <button type="button" onclick="getStagePositions();">Update</button>
        </p>

        <p>
          Doubleclick to position: <input type="checkbox" id="clickPositionCheck" checked><br>
          <div class="flexbox">
              <div class="flexgrow">FOV width:</div> 
              <div> 
                <input type="number" id="fovXText" onchange="fovX = Number(this.value); updateTextBoxes();" style="width: 7em" >
              </div> 
          </div>

          <div class="flexbox">
              <div class="flexgrow">FOV height:</div> 
              <div> 
                  <input type="number" id="fovYText" onchange="fovY = Number(this.value); updateTextBoxes();" style="width: 7em" >
              </div> 
          </div>
        </p>

        
        <h3>Move</h3>

        <p>
          <b>Up/down/left/right:</b> <br>
          Move stage in x-y. <br>
          <b>PgUp/PgDn: </b> <br>
          Move stage in z/focus.
        </p>

        <h4>Velocity:</h4>
        <div class="flexbox">
          <div class="flexgrow">x-y:</div>
          <div> 
            <input type="range" id="stageVelocityInput" min="50" max="200" oninput="stageVelocity = Number(this.value); updateTextBoxes();">
            <input type="text" id="stageVelocityText" size="3" disabled>
          </div>
        </div>
        <div class="flexbox">
          <div class="flexgrow">z:</div>
          <div> 
            <input type="range" id="focusVelocityInput" min="10" max="100" oninput="focusVelocity = Number(this.value); updateTextBoxes();">
            <input type="text" id="focusVelocityText" size="3" disabled>
          </div>
        </div>

    
        <p>Scroll to focus: <input type="checkbox" id="scrollFocusCheck" checked></p>

      </div>

      <div class="column middle">
        <div id="left-sb-btn" class="sb-btn">
          <button type="button" onclick="toggleClassToDiv('left-sb', 'hidden');">
              <img src="./static/icons/baseline-settings-20px.svg">
          </button>
        </div>
        <div id="right-sb-btn" class="sb-btn">
            <button type="button" onclick="toggleClassToDiv('right-sb', 'hidden');">
                <img src="./static/icons/baseline-camera_alt-24px.svg">
            </button>
        </div>
        <div id="streambox">No active stream</div>
      </div>

      <div class="column right" id="right-sb">
        <h2>Capture</h2>
        Filename: <br>
        <input type="text" id="captureFilenameInput" placeholder="Leave blank for default"><br>
        Store on Pi: <input type="checkbox" id="captureKeepOnDiskCheck"><br>
        Full resolution: <input type="checkbox" id="captureFullResolutionCheck"><br>
        Keep raw data: <input type="checkbox" id="captureBayerCheck"><br>
        Resize capture: <input type="checkbox" id="captureResizeCheck" onclick="newCaptureResizeToggle(this);"><br>
        <input type="number" id="captureWidthInput" value="640" style="width: 7em" disabled>
        <input type="number" id="captureHeightInput" value="480" style="width: 7em" disabled><br>

        <button type="button" onclick="newCaptureFromInput();">Capture</button>

        <h2>Captures</h2>
        <button type="button" onclick="getCaptures();">Update</button>
        <button type="button" onclick="deleteAllCaptures();">Delete all</button>
        <div id="captures"></div>
        
      </div>

    </div>

  </body>

</html>
+1 −0
Original line number Diff line number Diff line
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="none" d="M0 0h20v20H0V0z"/><path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/></svg>
 No newline at end of file

static/main.js

0 → 100644
+376 −0
Original line number Diff line number Diff line
var apiVersion = "v1"
var hostName
var hostPort
var baseURI

// Key IDs
var pgupKeyID = 33;
var pgdnKeyID = 34;

var leftKeyID = 37;
var upKeyID = 38;
var rightKeyID = 39;
var downKeyID = 40;

var enterKeyID = 13;
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() {
    setHostFromInput()
    getStagePositions()
    updateTextBoxes()
    getCaptures()
}


// Cosmetic methods
function toggleClassToDiv(div_id, class_name) {
    document.getElementById(div_id).classList.toggle(class_name)
}

// Setup methods
function setHostFromInput() {
    hostName = document.getElementById('microscopeHostInput').value;
    hostPort = document.getElementById('microscopePortInput').value;

    baseURI = `http://${hostName}:${hostPort}/api/${apiVersion}`

    streamContent = `<img src="${baseURI}/stream" ondblclick="clickHotspotImage(event);">`
    document.getElementById("streambox").innerHTML = streamContent;
}

// 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 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)
    }
}

function getCaptures() {
    function updateCapturesCallback(response, status) {
        console.log(status);
        updateCaptureList(response);
    }
    safeRequest("GET", baseURI+"/camera/capture", null, updateCapturesCallback, false)
}

function updateCaptureList(response) {
    // Clear captures list
    var capturesNode = document.getElementById("captures");
    while (capturesNode.firstChild) {
        capturesNode.removeChild(capturesNode.firstChild);
    }

    // For each capture in the response array
    response.forEach(function(element) {
        // Store the capture object URI
        element_uri = `${baseURI}/camera/capture/${element.metadata.id}`

        // Generate inner HTML from capture object
        html = `
        <div class="capture-thumb"><img src="${element_uri}/download?thumbnail=true"></div>
        <div class="capture-actions"> 
            <div class='capture-heading'>${element.metadata.filename}</div>
            <br>
            <button type="button" class="capture-button" onclick="window.open('${element_uri}/download');">Image</button>
            <button type="button" class="capture-button" onclick="window.open('${element_uri}/metadata');">Metadata</button>
            <br>
            <button type="button" class="capture-button" onclick="window.open('${element_uri}/', '_blank');">JSON</button>
            <button type="button" class="capture-button" onclick="deleteCapture('${element.metadata.id}');">Delete</button>
            <br>
        </div>
        `

        // Create a new capture div, and append HTML
        var div = document.createElement("div");
        div.className = "capture";
        div.innerHTML = html;
        capturesNode.appendChild(div);
    });

}

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, keep_on_disk, 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,
        "keep_on_disk": keep_on_disk,
        "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');
    captureKeepOnDiskCheck = document.getElementById('captureKeepOnDiskCheck');
    captureFullResolutionCheck = document.getElementById('captureFullResolutionCheck');
    captureBayerCheck = document.getElementById('captureBayerCheck')

    captureResizeCheck = document.getElementById('captureResizeCheck');
    captureWidthInput = document.getElementById('captureWidthInput')
    captureHeightInput = document.getElementById('captureHeightInput')

    // Get filename if one is given
    if (captureFilenameInput.value === "") {
        filename = null;
    }
    else {
        filename = captureFilenameInput.value;
    }

    if (captureResizeCheck.checked) {
        resizeWidth = Number(captureWidthInput.value);
        resizeHeight = Number(captureHeightInput.value);
    }
    else {
        resizeWidth = null;
        resizeHeight = null;
    }

    newCapture(
        filename, 
        captureKeepOnDiskCheck.checked, 
        !(captureFullResolutionCheck.checked), 
        captureBayerCheck.checked, 
        resizeWidth, 
        resizeHeight
    );

}

function newCaptureResizeToggle(checkbox) {
    captureWidthInput = document.getElementById('captureWidthInput')
    captureHeightInput = document.getElementById('captureHeightInput')

    captureWidthInput.disabled = !(checkbox.checked)
    captureHeightInput.disabled = !(checkbox.checked)
}

// 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);
}

// Methods for input events

// Click-to-move
function clickHotspotImage(event) {
    if (document.getElementById("clickPositionCheck").checked == true) {
        xCoordinate = event.offsetX;
        yCoordinate = event.offsetY;
        console.log(xCoordinate, yCoordinate)
        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;
        console.log(xSteps, ySteps, 0)
        // Make a position request
        moveStagePositions(xSteps, ySteps, 0)
    }
}

// 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")) {
        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)
    }
});

// Keyboard to move
addEventListener("keydown", function (e) {
    keysDown[e.keyCode] = true; //Add key to array

    // If not currently in an input box
    if (!(e.target instanceof HTMLInputElement)) {

        // TODO: Remove this if condition? Pointless?
        // If stage movement keys are pressed
        if ((leftKeyID in keysDown) || (rightKeyID in keysDown) || (upKeyID in keysDown) || (downKeyID in keysDown) || (pgupKeyID in keysDown) || (pgdnKeyID in keysDown)) {
            // Calculate movement array
            x_rel = 0;
            y_rel = 0;
            z_rel = 0;
            if (leftKeyID in keysDown) {
            x_rel = x_rel + stageVelocity;
            }
            if (rightKeyID in keysDown) {
            x_rel = x_rel - stageVelocity;
            }
            if (upKeyID in keysDown) {
            y_rel = y_rel + stageVelocity;
            }
            if (downKeyID in keysDown) {
            y_rel = y_rel - stageVelocity;
            }
            if (pgupKeyID in keysDown) {
            z_rel = z_rel - focusVelocity;
            }
            if (pgdnKeyID in keysDown) {
            z_rel = z_rel + focusVelocity;
            }

            // Make a position request
            moveStagePositions(x_rel, y_rel, z_rel)
        }
    }

}, false);

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)
    }
}

static/style.css

0 → 100644
+132 −0
Original line number Diff line number Diff line
body {
    margin: 0;
    overflow: hidden;
    font-family: sans-serif;
    font-size: 14px;
}

* {
    box-sizing: border-box;
}

/* Preview sizing */
.middle img {  
    width: 100%;
    height: 100%;
    object-fit: contain
}

/* Create columns that float next to each other */
.column {
    padding: 10px;
    height: 100%;
}

.left, .right {
  background-color: #f4f4f4;
  overflow-y: scroll;
  flex: 0;
  flex-shrink: 0;
  flex-basis: 250px;
  -webkit-transition: all 0.2s ease-in-out;
  transition: all 0.2s ease-in-out;
}

.left{
    float: left;
}

.right{
    float: right;
}

.hidden{
    flex-basis: 0;
    padding:0
}

.middle {
  position: relative;
  flex-grow: 1;
  background-color: #d3d3d3;
}

#left-sb-btn {
    left: 20px;
}

#right-sb-btn {
    right: 20px;
}

.sb-btn {
    top: 20px;
    position: absolute;
}

.sb-btn button{
    width: 40px;
    height: 40px;
    margin: 0;
    padding:5px;  
}

/* Clear floats after the columns */
.row{
    display: flex;
}

/* Flexboxes */
.flexbox {
    display: flex;
    margin-bottom: 5px;
}

.flexgrow {
    flex-grow: 1;
}

/* Data divs */

#captures {
    font-size:12px;
    margin-top: 20px;
}

.capture {
    height: 75px;
    width: 240px;
    margin-bottom: 5px;
    background-color: #d3d3d3;
    display: flex;
}

.capture-thumb {
    width: 80px;
    height: 75px;
}

.capture-heading {
    font-weight: bold;
    margin: 5px 0 5px 0;
    display: inline-block;
}

.capture-actions {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-left: 5px;
}

.capture img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.capture-button {
    width: 75px;
    margin-bottom: 3px;
    height: 22px;
}
 No newline at end of file