Replace tasksubmitter with action-button

I've changed tasksubmitter to accept a "thing" and "action" rather than a single URL. This seems more in keeping with
propertyControl, and eliminates the need for loads of
computed properties.

I've also deleted (rather than just commenting out) chunks of the web app that are no longer in use. These may be
reinstated in the future - but we can get them from git
history. It's less confusing not to have vestigial
code in the repo.

This builds OK but is not, as yet, tested with hardware.
This commit is contained in:
Richard Bowman 2024-02-29 21:44:39 +00:00
parent 612b88d58c
commit cbd8b090ec
22 changed files with 225 additions and 2325 deletions

View file

@ -429,7 +429,7 @@ html {
padding: 0;
}
.image-fit{
.image-fit {
height: 80%;
width: 100%;
object-fit: contain;

View file

@ -7,7 +7,6 @@
<!-- Initialisation modals -->
<calibrationModal
ref="calibrationModal"
:available-plugins="plugins"
@onClose="enterApp()"
></calibrationModal>
<!-- Vertical tab bar -->
@ -41,22 +40,6 @@
<hr v-if="item.divide" :key="'tab-divider-' + index" />
</template>
<!-- For each plugin tab -->
<tabIcon
v-for="plugin in pluginsGuiList"
:key="plugin.id"
:tab-i-d="plugin.id"
:title="plugin.title"
:require-connection="plugin.requiresConnection"
:current-tab="currentTab"
:click-callback="updatePlugins"
@set-tab="setTab"
>
<span class="material-symbols-outlined">{{
plugin.icon || "extension"
}}</span>
</tabIcon>
<tabIcon
v-for="imjoyTab in imjoyTabs"
:key="imjoyTab.id"
@ -115,23 +98,6 @@
<component :is="item.component"></component>
</tabContent>
<!-- For each plugin tab -->
<tabContent
v-for="plugin in pluginsGuiList"
:key="plugin.id"
:tab-i-d="plugin.id"
:require-connection="plugin.requiresConnection"
:current-tab="currentTab"
>
<extensionContent
:forms="plugin.forms"
:frame="plugin.frame"
:web-component="plugin.wc"
:view-panel="plugin.viewPanel"
@reloadForms="updatePlugins()"
/>
</tabContent>
<tabContent
v-for="imjoyTab in imjoyTabs"
:key="imjoyTab.id"
@ -166,19 +132,13 @@ import tabContent from "./genericComponents/tabContent";
// Import new content components
import navigateContent from "./tabContentComponents/navigateContent.vue";
import captureContent from "./tabContentComponents/captureContent.vue";
import slideScanContent from "./tabContentComponents/slideScanContent.vue";
import backgroundDetectContent from "./tabContentComponents/backgroundDetectContent.vue";
import viewContent from "./tabContentComponents/viewContent.vue";
import settingsContent from "./tabContentComponents/settingsContent.vue";
import extensionContent from "./tabContentComponents/extensionContent.vue";
import aboutContent from "./tabContentComponents/aboutContent.vue";
import loggingContent from "./tabContentComponents/loggingContent.vue";
// ImJoy and the gallery are loaded asynchronously to allow them to be disabled if needed
const galleryContent = () =>
import(
/* webpackChunkName: "gallery" */ "./tabContentComponents/galleryContent.vue"
);
// ImJoy is loaded asynchronously to allow it to be disabled if needed
const imjoyContent = () =>
import(
/* webpackChunkName: "imjoy" */ "./tabContentComponents/imjoyContent.vue"
@ -197,12 +157,9 @@ export default {
tabIcon,
tabContent,
navigateContent,
captureContent,
slideScanContent,
viewContent,
settingsContent,
galleryContent,
extensionContent,
calibrationModal,
aboutContent,
loggingContent,
@ -211,7 +168,6 @@ export default {
},
data: function() {
return {
plugins: [],
currentTab: "view",
bottomTabs: [
{
@ -235,25 +191,11 @@ export default {
},
computed: {
pluginsGuiList: function() {
// List of plugin GUIs, obtained from this.plugins values
var pluginGuis = [];
for (let plugin of Object.values(this.plugins)) {
if (plugin.meta.gui) {
pluginGuis.push(plugin.meta.gui);
}
}
return pluginGuis;
},
tabOrder: function() {
var ind = [];
for (const tab of this.topTabs) {
ind.push(tab.id);
}
for (const plugin of this.pluginsGuiList) {
ind.push(plugin.id);
}
for (const tab of this.bottomTabs) {
ind.push(tab.id);
}
@ -272,11 +214,6 @@ export default {
icon: "gamepad",
component: navigateContent
},
/*{
id: "gallery",
icon: "photo_library",
component: galleryContent
},*/
{
id: "slidescan",
icon: "settings_overscan",
@ -318,11 +255,7 @@ export default {
created: function() {
if (this.$store.getters.ready) {
// Update plugins
this.updatePlugins().then(() => {
// Start initialisation modals
this.startModals();
});
this.startModals();
}
},
@ -342,19 +275,6 @@ export default {
},
methods: {
updatePlugins: function() {
/*return axios
.get(this.pluginsUri)
.then(response => {
this.plugins = response.data;
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});*/
return new Promise(resolve => {
resolve({});
});
},
setTab: function(event, tab) {
if (!(this.currentTab == tab)) {
this.currentTab = tab;

View file

@ -72,15 +72,19 @@
<script>
import axios from "axios";
import UIkit from "uikit";
import actionProgressBar from "./actionProgressBar.vue";
import ActionProgressBar from "./actionProgressBar.vue";
import ActionLogDisplay from "./actionLogDisplay.vue";
export default {
name: "TaskSubmitter",
components: { actionProgressBar, ActionLogDisplay },
name: "ActionButton",
components: { ActionProgressBar, ActionLogDisplay },
props: {
submitUrl: {
action: {
type: String,
required: true
},
thing: {
type: String,
required: true
},
@ -142,6 +146,12 @@ export default {
};
},
computed: {
submitUrl() {
return this.thingActionUrl(this.thing, this.action);
}
},
watch: {
progress(newval) {
this.$emit("update:progress", newval);

View file

@ -1,5 +1,8 @@
<template>
<div ref="logContainer" class="log-container uk-margin-left uk-margin-right uk-margin">
<div
ref="logContainer"
class="log-container uk-margin-left uk-margin-right uk-margin"
>
<div v-if="log">
<div v-for="(item, index) in log" :key="`log_entry_${index}`">
{{ item.message }}

View file

@ -1,6 +1,7 @@
<template>
<div>
<label v-if="dataType == 'number'" class="uk-form-label">{{ label }}
<label v-if="dataType == 'number'" class="uk-form-label"
>{{ label }}
<div class="input-and-buttons-container">
<input
v-model="value"
@ -30,7 +31,8 @@
<span class="material-symbols-outlined">refresh</span>
</a>
</div>
<label v-if="dataType == 'number_array'" class="uk-form-label">{{ label }}
<label v-if="dataType == 'number_array'" class="uk-form-label"
>{{ label }}
<div class="input-and-buttons-container">
<input
v-for="i in valueLength"
@ -47,7 +49,8 @@
</a>
</div>
</label>
<label v-if="dataType == 'number_object'" class="uk-form-label">{{ label }}
<label v-if="dataType == 'number_object'" class="uk-form-label"
>{{ label }}
<div class="input-and-buttons-container">
<input
v-for="(_v, key) in value"
@ -64,7 +67,8 @@
</a>
</div>
</label>
<label v-if="dataType == 'other'" class="uk-form-label">{{ label }}
<label v-if="dataType == 'other'" class="uk-form-label"
>{{ label }}
<div class="input-and-buttons-container">
<input
:value="value"

View file

@ -1,249 +0,0 @@
<template>
<div class="uk-padding-small">
<div class="uk-flex">
<div class="uk-text-bold uk-text-uppercase uk-width-expand">
{{ name }}
</div>
<a href="#" class="uk-icon uk-width-auto" @click="updateForm()"
><span class="material-symbols-outlined">cached</span></a
>
</div>
<form ref="formContainer" class="uk-form-stacked" @submit.prevent="">
<div v-for="(field, index) in schema" :key="index">
<div
v-if="Array.isArray(field)"
class="uk-grid-small uk-width-1-1 uk-child-width-expand"
uk-grid
>
<div v-for="(subfield, subindex) in field" :key="subindex">
<component
:is="subfield.fieldType"
v-model="formData[subfield.name]"
v-bind="subfield"
>
</component>
</div>
</div>
<component
:is="field.fieldType"
v-model="formData[field.name]"
v-bind="field"
>
</component>
</div>
<div v-if="isTask" class="uk-margin">
<taskSubmitter
:submit-url="submitApiUri"
:submit-data="formData"
:submit-label="submitLabel"
@submit="onTaskSubmit"
@response="onTaskResponse"
@error="onTaskError"
>
</taskSubmitter>
</div>
<div v-else class="uk-margin">
<button
type="button"
class="uk-button uk-button-primary uk-width-1-1"
@click="newQuickRequest(formData)"
>
{{ submitLabel }}
</button>
</div>
</form>
</div>
</template>
<script>
import axios from "axios";
import numberInput from "../fieldComponents/numberInput";
import selectList from "../fieldComponents/selectList";
import textInput from "../fieldComponents/textInput";
import htmlBlock from "../fieldComponents/htmlBlock";
import radioList from "../fieldComponents/radioList";
import checkList from "../fieldComponents/checkList";
import tagList from "../fieldComponents/tagList";
import keyvalList from "../fieldComponents/keyvalList";
import taskSubmitter from "../genericComponents/taskSubmitter";
export default {
name: "JsonForm",
components: {
numberInput,
selectList,
textInput,
htmlBlock,
radioList,
checkList,
tagList,
keyvalList,
taskSubmitter
},
props: {
name: {
type: String,
required: false,
default: "Plugin"
},
schema: {
type: Array,
required: true
},
route: {
type: String,
required: true
},
isTask: {
type: Boolean,
required: false,
default: false
},
submitLabel: {
type: String,
required: false,
default: "Submit"
},
emitOnResponse: {
type: String,
required: false,
default: null
}
},
data: function() {
return {
formData: {}
};
},
computed: {
pluginApiUri: function() {
return `${this.$store.getters.baseUri}/api/v2/extensions`;
},
submitApiUri: function() {
return this.pluginApiUri + this.route;
}
},
watch: {
// Whenever the form schema updates, re-check server for field values
schema: function() {
this.updateFormValues();
}
},
created() {
this.initialiseFormData();
},
methods: {
initialiseFormData() {
/*
This function initialises the form data.
Limitations in Vue mean that newly created formData properties are not reactive.
GETting formData values from the server creates the properties, but the form components
don't get updated because of this limitation.
Here, we step through the form schema, and properly create reactive properties for each component.
*/
for (const field of this.schema) {
if (Array.isArray(field)) {
var defaultValue; // Initial value of the form component
for (const subfield of field) {
// If a default value is given in the schema, use this
if (subfield.value) {
defaultValue = subfield.value;
} else if (subfield.default) {
defaultValue = subfield.default;
} else {
defaultValue = null;
}
this.$set(this.formData, subfield.name, defaultValue);
}
} else {
// If a default value is given in the schema, use this
if (field.value) {
defaultValue = field.value;
} else if (field.default) {
defaultValue = field.default;
} else {
defaultValue = null;
}
this.$set(this.formData, field.name, defaultValue);
}
}
},
updateFormValues() {
for (const field of this.schema) {
if (Array.isArray(field)) {
for (const subfield of field) {
// If a default value is given in the schema, use this
if (subfield.value) {
this.$set(this.formData, subfield.name, subfield.value);
}
}
} else {
if (field.value) {
this.$set(this.formData, field.name, field.value);
}
}
}
},
updateForm() {
this.$emit("reloadForms");
},
onSubmissionCompleted: function() {
if (this.emitOnResponse) {
this.$root.$emit(this.emitOnResponse);
}
this.updateForm();
},
newQuickRequest: function(params) {
// Send a quick request
axios
.post(this.submitApiUri, params)
.then(() => {
// Do all the finished request stuff
this.onSubmissionCompleted();
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
onTaskSubmit: function() {},
onTaskResponse: function() {
// Do all the finished request stuff
this.onSubmissionCompleted();
},
onTaskError: function(error) {
this.modalError(error);
}
}
};
</script>
<style scoped>
.flex-container {
display: flex;
}
.flex-container > div {
flex-basis: 100%;
}
</style>

View file

@ -12,12 +12,12 @@
<br />
{{ $store.state.origin }}
</div>
<task-submitter
v-if="flashLedUri"
:submit-url="flashLedUri"
<action-button
thing="stage"
action="flash_led"
submit-label="Flash Illumination"
:can-terminate="false"
:submit-data="{'dt': 0.25}"
:submit-data="{ dt: 0.25 }"
/>
</div>
@ -83,18 +83,15 @@
<script>
import axios from "axios";
import taskSubmitter from '../../genericComponents/taskSubmitter.vue';
import ActionButton from "../../labThingsComponents/actionButton.vue";
export default {
components: { taskSubmitter },
name: "StatusPane",
components: { ActionButton },
computed: {
things: function() {
return this.$store.getters["wot/thingDescriptions"];
},
flashLedUri() {
return this.thingActionUrl("stage", "flash_led");
}
},

View file

@ -5,39 +5,41 @@
</div>
<div v-show="backendOK">
<ul uk-accordion="multiple: true">
<li>
<a class="uk-accordion-title" href="#">Settings</a>
<div class="uk-accordion-content">
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="tolerance"
label="Tolerance"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="fraction"
label="Sample coverage required (%)"
/>
</div>
<div class="uk-margin">
<taskSubmitter
:submit-url="backgroundFractionUri"
submit-label="Check coverage"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundFraction"
@error="backgroundDetectError"
/>
</div>
<li>
<a class="uk-accordion-title" href="#">Settings</a>
<div class="uk-accordion-content">
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="tolerance"
label="Tolerance"
/>
</div>
</li>
</ul>
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="fraction"
label="Sample coverage required (%)"
/>
</div>
<div class="uk-margin">
<action-button
thing="background_detect"
action="background_fraction"
submit-label="Check coverage"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundFraction"
@error="backgroundDetectError"
/>
</div>
</div>
</li>
</ul>
<div class="uk-margin">
<taskSubmitter
:submit-url="setBackgroundUri"
<action-button
thing="background_detect"
action="set_background"
submit-label="Set background"
:can-terminate="false"
:poll-interval="0.1"
@ -45,8 +47,9 @@
/>
</div>
<div class="uk-margin">
<taskSubmitter
:submit-url="labelImageUri"
<action-button
thing="background_detect"
action="image_is_sample"
submit-label="Check current image"
:can-terminate="false"
:poll-interval="0.1"
@ -59,29 +62,18 @@
</template>
<script>
import taskSubmitter from "../../genericComponents/taskSubmitter";
import ActionButton from "../../labThingsComponents/actionButton.vue";
import propertyControl from "../../labThingsComponents/propertyControl.vue";
export default {
components: {
taskSubmitter,
ActionButton,
propertyControl
},
computed: {
labelImageUri() {
return this.thingActionUrl("background_detect", "image_is_sample");
},
backgroundFractionUri() {
return this.thingActionUrl("background_detect", "background_fraction");
},
setBackgroundUri() {
return this.thingActionUrl("background_detect", "set_background");
},
backendOK() {
return (
(this.backgroundFractionUri != null) & (this.setBackgroundUri != null)
);
return this.thingAvailable("background_detect");
}
},

View file

@ -1,547 +0,0 @@
<template>
<div id="paneCapture" class="uk-padding-small">
<div>
<label class="uk-form-label" for="form-stacked-text">Filename</label>
<input
v-model="filename"
class="uk-input uk-width-1-1 uk-form-small"
name="inputFilename"
placeholder="Leave blank for default"
/>
</div>
<p uk-tooltip="title: Capture will be removed automatically; delay: 500">
<label
><input v-model="temporary" class="uk-checkbox" type="checkbox" />
Temporary</label
>
</p>
<hr />
<div class="uk-child-width-1-2" uk-grid>
<p>
<label
><input
v-model="fullResolution"
class="uk-checkbox"
type="checkbox"
/>
Full resolution</label
>
</p>
<p>
<label
><input v-model="storeBayer" class="uk-checkbox" type="checkbox" />
Store raw data</label
>
</p>
</div>
<hr />
<p>
<label
><input v-model="resizeCapture" class="uk-checkbox" type="checkbox" />
Resize capture</label
>
</p>
<div class="uk-child-width-1-2" uk-grid>
<div>
<input
v-model="resizeDims[0]"
:class="resizeClass"
class="uk-input uk-form-width-medium uk-form-small"
type="number"
name="inputResizeW"
/>
</div>
<div>
<input
v-model="resizeDims[1]"
:class="resizeClass"
class="uk-input uk-form-width-medium uk-form-small"
type="number"
name="inputResizeH"
/>
</div>
</div>
<ul uk-accordion="multiple: true">
<li>
<a class="uk-accordion-title" href="#">Notes</a>
<div class="uk-accordion-content">
<div class="uk-margin-small">
<textarea
v-model="captureNotes"
class="uk-textarea"
rows="5"
placeholder="Capture notes"
></textarea>
</div>
</div>
</li>
<li>
<a class="uk-accordion-title" href="#">Annotations</a>
<div class="uk-accordion-content">
<keyvalList v-model="annotations" />
</div>
</li>
<li>
<a class="uk-accordion-title" href="#">Tags</a>
<div class="uk-accordion-content">
<tagList v-model="tags" />
</div>
</li>
</ul>
<hr />
<ul uk-accordion="multiple: true">
<!--Show stack and scan if scan plugin is enabled-->
<li v-if="thingAvailable('scan')" :class="{ 'uk-open': scanCapture }">
<a class="uk-accordion-title" href="#">Stack and Scan</a>
<div class="uk-accordion-content">
<div class="uk-margin">
<label
><input
v-model="scanCapture"
class="uk-checkbox"
type="checkbox"
/>
Scan capture</label
>
</div>
<div :class="{ 'uk-disabled': !scanCapture }">
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
<div>
<label class="uk-form-label" for="form-stacked-text"
>x step-size</label
>
<div class="uk-form-controls">
<input
v-model="scanStepSize.x"
class="uk-input uk-form-small"
type="number"
name="inputPositionX"
/>
</div>
</div>
<div>
<label class="uk-form-label" for="form-stacked-text"
>y step-size</label
>
<div class="uk-form-controls">
<input
v-model="scanStepSize.y"
class="uk-input uk-form-small"
type="number"
name="inputPositionY"
/>
</div>
</div>
<div>
<label class="uk-form-label" for="form-stacked-text"
>z step-size</label
>
<div class="uk-form-controls">
<input
v-model="scanStepSize.z"
class="uk-input uk-form-small"
type="number"
name="inputPositionZ"
/>
</div>
</div>
</div>
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
<div>
<label class="uk-form-label" for="form-stacked-text"
>x steps</label
>
<div class="uk-form-controls">
<input
v-model="scanSteps.x"
class="uk-input uk-form-small"
type="number"
name="inputPositionX"
min="1"
/>
</div>
</div>
<div>
<label class="uk-form-label" for="form-stacked-text"
>y steps</label
>
<div class="uk-form-controls">
<input
v-model="scanSteps.y"
class="uk-input uk-form-small"
type="number"
name="inputPositionY"
min="1"
/>
</div>
</div>
<div>
<label class="uk-form-label" for="form-stacked-text"
>z steps</label
>
<div class="uk-form-controls">
<input
v-model="scanSteps.z"
class="uk-input uk-form-small"
type="number"
name="inputPositionZ"
min="1"
/>
</div>
</div>
</div>
<div class="uk-margin-small uk-margin-remove-bottom">
<label class="uk-form-label" for="form-stacked-text"
>Autofocus</label
>
<select v-model="scanDeltaZ" class="uk-select">
<option>Off</option>
<option>Coarse</option>
<option>Medium</option>
<option>Fine</option>
<option>Fast</option>
</select>
</div>
<div
v-if="thingAvailable('background_detect')"
class="uk-margin-small uk-margin-remove-bottom"
>
<label class="uk-form-label" for="form-stacked-text">
<input
v-model="detectEmptyFieldsAndSkipAutofocus"
class="uk-checkbox"
type="checkbox"
/>
Skip autofocus for empty fields of view
</label>
</div>
<div class="uk-margin-small uk-margin-remove-bottom">
<label class="uk-form-label" for="form-stacked-text"
>Scan Style</label
>
<select v-model="scanStyle" class="uk-select">
<option>Raster</option>
<option>Snake</option>
<option>Spiral</option>
</select>
</div>
<div class="uk-margin-small uk-margin-remove-bottom">
<label class="uk-form-label" for="form-stacked-text"
>Naming style</label
>
<select v-model="namingStyle" class="uk-select">
<option>Coordinates</option>
<option>Number</option>
</select>
</div>
</div>
</div>
</li>
<!--Show stack and scan if scan plugin is enabled-->
<li
v-if="thingAvailable('smart_scan')"
:class="{ 'uk-open': smartStack && scanCapture }"
>
<a class="uk-accordion-title" href="#">Smart Stack</a>
<div
class="uk-accordion-content"
:class="{ 'uk-disabled': !scanCapture }"
>
<div class="uk-margin">
<label
><input
v-model="smartStack"
class="uk-checkbox"
type="checkbox"
/>
Enable smart stacking</label
>
</div>
<p>
Smart stacking is an experimental closed-loop Z stack, that checks
the Z stack is centred on the focus. It requires Z stacking to be
enabled above (we recommend 9 images in Z) and you must select
"fast" autofocus.
</p>
<div :class="{ 'uk-disabled': !smartStack }">
<div class="uk-margin-small uk-margin-remove-bottom">
<label class="uk-form-label" for="form-stacked-text"
>Autofocus range (steps)</label
>
<input
v-model="smartStackAutofocusDz"
class="uk-input uk-form-small"
type="number"
min="1000"
/>
</div>
<div class="uk-margin-small uk-margin-remove-bottom">
<label class="uk-form-label" for="form-stacked-text"
>Alignment range (steps)</label
>
<input
v-model="smartStackAlignDist"
class="uk-input uk-form-small"
type="number"
min="100"
/>
</div>
</div>
</div>
</li>
</ul>
<div v-if="scanCapture" class="uk-margin uk-margin-remove-top">
<taskSubmitter
v-if="smartStack"
:submit-url="smartScanUri"
:submit-data="smartScanPayload"
:submit-label="'Start Smart Scan'"
:button-primary="true"
@response="onScanResponse"
@error="modalError"
>
</taskSubmitter>
<taskSubmitter
v-if="!smartStack"
:submit-url="scanUri"
:submit-data="scanPayload"
:submit-label="'Start Scan'"
:button-primary="true"
@response="onScanResponse"
@error="modalError"
>
</taskSubmitter>
</div>
<button
v-else
class="uk-button uk-button-primary uk-margin uk-margin-remove-top uk-width-1-1"
@click="handleCapture()"
>
Capture
</button>
</div>
</template>
<script>
import axios from "axios";
import tagList from "../../fieldComponents/tagList";
import keyvalList from "../../fieldComponents/keyvalList";
import taskSubmitter from "../../genericComponents/taskSubmitter";
import { syncDataWithLocalStorage } from "../../../syncDataWithLocalStorage";
/**
* The capture settings that should persist in local storage are these ones
*/
function defaultCaptureSettings() {
return {
filename: "",
temporary: false,
fullResolution: false,
storeBayer: false,
resizeCapture: false,
captureNotes: "",
scanDeltaZ: "Fast",
scanStyle: "Raster",
namingStyle: "Coordinates",
scanStepSize: {
x: 800,
y: 640,
z: 50
},
scanSteps: {
x: 3,
y: 3,
z: 5
},
resizeDims: [640, 480],
tags: [],
annotations: {
Client: "openflexure-microscope-jsclient:builtin"
},
detectEmptyFieldsAndSkipAutofocus: false,
smartStack: false,
smartStackThreshold: 0.9,
smartStackPeakWidth: 200,
smartStackAlignDist: 900,
smartStackBacklash: 100,
smartStackFitStyle: "chevy",
smartStackMAndMIndex: 10,
smartStackAutofocusDz: 3000
};
}
// Export main app
export default {
name: "PaneCapture",
components: {
tagList,
keyvalList,
taskSubmitter
},
data: function() {
return {
...defaultCaptureSettings(),
scanCapture: false // Don't remember the "scan" tickbox in local storage.
};
},
computed: {
resizeClass: function() {
return {
"uk-disabled": !this.resizeCapture
};
},
captureActionUri: function() {
return this.thingActionUrl("camera", "capture");
},
scanUri: function() {
return this.thingActionUrl("scan", "scan");
},
smartScanUri: function() {
return this.thingActionUrl("scan", "scan");
},
basePayload: function() {
var payload = {};
// Filename
if (this.filename) {
payload.filename = this.filename;
}
// Basic boolean params
payload.temporary = this.temporary;
payload.use_video_port = !this.fullResolution;
payload.bayer = this.storeBayer;
// Resizing
if (this.resizeCapture) {
payload.resize = {
width: this.resizeDims[0],
height: this.resizeDims[1]
};
}
// Additional annotations
payload.annotations = this.annotations;
payload.tags = this.tags;
// Attach notes
if (this.captureNotes) {
payload.annotations["Notes"] = this.captureNotes;
}
return payload;
},
scanPayload: function() {
// Convert AF selector to dz
var afDeltas = {
Off: 0,
Coarse: 100,
Medium: 30,
Fine: 10,
Fast: 2000
};
return {
...this.basePayload,
// Scan params
grid: [this.scanSteps.x, this.scanSteps.y, this.scanSteps.z],
stride_size: [
this.scanStepSize.x,
this.scanStepSize.y,
this.scanStepSize.z
],
style: this.scanStyle.toLowerCase(),
namemode: this.namingStyle.toLowerCase(),
autofocus_dz: afDeltas[this.scanDeltaZ],
fast_autofocus: this.scanDeltaZ == "Fast",
detect_empty_fields_and_skip_autofocus: this
.detectEmptyFieldsAndSkipAutofocus
};
},
smartScanPayload: function() {
return {
...this.scanPayload,
threshold: this.smartStackThreshold,
width: this.smartStackPeakWidth,
align_dist: this.smartStackAlignDist,
backlash: this.smartStackBacklash,
fit_style: this.smartStackFitStyle,
m_and_m_index: this.smartStackMAndMIndex,
autofocus_dz: this.smartStackAutofocusDz
};
}
},
mounted() {
// Load settings if they have been saved, and set up watchers to sync with local storage
syncDataWithLocalStorage("captureSettings", this, defaultCaptureSettings());
// A global signal listener to perform a capture action
this.$root.$on("globalCaptureEvent", () => {
this.handleCapture();
});
},
methods: {
handleCapture: function() {
var payload = this.basePayload;
// Do capture
axios
.post(this.captureActionUri, payload)
.then(() => {
// Flash the stream (capture animation)
this.$root.$emit("globalFlashStream");
// Update the global capture list
this.$root.$emit("globalUpdateCaptures");
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
onScanResponse: function() {
this.modalNotify("Finished scan.");
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
}
}
};
</script>
<style lang="less">
.deletable-label {
cursor: pointer;
}
.deletable-label:hover {
background-color: #f0506e;
}
</style>

View file

@ -3,23 +3,7 @@
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<div class="control-component">
<vue-friendly-iframe v-if="frame" :src="frame.href"></vue-friendly-iframe>
<!-- Handle OpenFlexure Forms -->
<div
v-for="form in forms"
v-else-if="forms"
:key="`${form.route}/${form.name}`.replace(/\s+/g, '-').toLowerCase()"
class="uk-height-1-1 uk-width-1-1"
>
<JsonForm
:name="form.name"
:route="form.route"
:is-task="form.isTask"
:submit-label="form.submitLabel"
:schema="form.schema"
:emit-on-response="form.emitOnResponse"
v-on="$listeners"
/>
</div>
<!-- OpenFlexure Forms used to be handled here - should now be done another way. -->
</div>
<div class="view-component uk-width-expand">
<galleryContent v-if="viewPanel == 'gallery'" />
@ -30,7 +14,6 @@
</template>
<script>
import JsonForm from "../pluginComponents/JsonForm";
import streamDisplay from "./streamContent.vue";
import galleryContent from "../tabContentComponents/galleryContent.vue";
import settingsContent from "../tabContentComponents/settingsContent.vue";
@ -39,7 +22,6 @@ export default {
name: "ExtensionContent",
components: {
JsonForm,
streamDisplay,
galleryContent,
settingsContent

View file

@ -1,392 +0,0 @@
<template>
<div
class="capture-card uk-card uk-card-default uk-padding-remove uk-width-medium"
:class="{ 'uk-card-secondary': $store.state.darkMode }"
>
<div class="uk-card-media-top">
<a class="lightbox-link" :href="imgURL" :data-caption="name">
<img
class="uk-width-1-1"
:data-src="thumbURL"
:alt="id"
width="300"
height="225"
uk-img
/>
</a>
</div>
<div class="uk-card-body uk-padding-small">
<div
class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right"
uk-grid
>
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand">
{{ name }}
</div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto">
<a href="#" class="uk-icon" @click="delCaptureConfirm()">
<span class="material-symbols-outlined">delete</span>
</a>
</div>
</div>
<div
class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-expand"
>
<time>{{ time }}</time>
</div>
<div
class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-auto"
>
<a :href="metadataModalTarget" uk-toggle>More...</a>
</div>
</div>
<div class="uk-card-footer uk-padding-small">
<button
v-if="openInImjoyMenuItems.length != 0"
class="uk-icon"
type="button"
>
<img
style="width:25px;"
src="https://imjoy.io/static/img/imjoy-icon.svg"
/>
</button>
<div uk-dropdown="pos: top-center">
<ul class="uk-nav uk-dropdown-nav">
<li v-for="item in openInImjoyMenuItems" :key="item.name">
<a href="#" @click="item.callback(name, imgURL)"
><span class="material-symbols-outlined">launch</span
>{{ item.title }}</a
>
</li>
</ul>
</div>
&nbsp;
<div v-for="tag in tags" :key="tag" class="uk-display-inline">
<span
v-if="tag === 'temporary'"
class="uk-label uk-label-danger uk-margin-small-right"
uk-tooltip="title: Capture will be removed automatically; delay: 500"
>Temporary</span
>
<span
v-else
class="uk-label uk-margin-small-right deletable-label"
@click="delTagConfirm(tag)"
>
{{ tag }}
</span>
</div>
<a :href="tagModalTarget" uk-toggle>
<span class="uk-label uk-label-success uk-margin-small-right">Add</span>
</a>
</div>
<!-- Metadata modal -->
<div :id="metadataModalID" uk-modal>
<div
class="uk-modal-dialog uk-modal-body"
:class="{
'uk-light uk-background-secondary': $store.state.darkMode
}"
>
<button class="uk-modal-close-default" type="button" uk-close></button>
<h2 class="uk-modal-title">{{ name }}</h2>
<p><b>Path: </b>{{ path }}</p>
<p><b>Time: </b>{{ time }}</p>
<p><b>ID: </b>{{ id }}</p>
<p><b>Format: </b>{{ format }}</p>
<hr />
<div v-for="(value, key) in annotations" :key="key">
<p>
<b>{{ key }}: </b>{{ value }}
</p>
</div>
<hr />
<div class="uk-flex-bottom" uk-grid>
<div class="uk-width-2-3">
<keyvalList v-model="newAnnotations" />
</div>
<div class="uk-width-1-3">
<button
class="uk-button uk-button-primary uk-form-small uk-width-1-1"
@click="handleAnnotationsSubmit()"
>
Add annotation
</button>
</div>
</div>
</div>
</div>
<!-- New tag modal -->
<div :id="tagModalID" uk-modal>
<form
class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical"
:class="{
'uk-light uk-background-secondary': $store.state.darkMode
}"
@submit.prevent="handleTagSubmit"
>
<div class="uk-inline">
<span class="uk-form-icon"
><span class="material-symbols-outlined">label</span></span
>
<input
v-model="newTag"
autofocus
class="uk-input uk-form-width-medium uk-form-small"
type="text"
name="tagname"
placeholder="tag"
/>
<button
class="uk-button uk-button-default uk-margin-left uk-form-small uk-modal-close"
type="button"
>
Cancel
</button>
<button
type="submit"
class="uk-button uk-button-primary uk-margin-left uk-form-small"
>
Save
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import UIkit from "uikit";
import axios from "axios";
import { mapState } from "vuex";
import keyvalList from "../../fieldComponents/keyvalList";
// Export main app
export default {
name: "CaptureCard",
components: {
keyvalList
},
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
time: {
type: String,
required: true
},
path: {
type: String,
required: true
},
format: {
type: String,
required: true
},
links: {
type: Object,
required: true
},
tags: {
type: Array,
required: false,
default: function() {
return [];
}
},
annotations: {
type: Object,
required: false,
default: function() {
return {};
}
}
},
data: function() {
return {
newTag: "",
newAnnotations: {}
};
},
computed: {
tagModalID: function() {
return this.makeModalName("tag-modal-");
},
tagModalTarget: function() {
return "#" + this.tagModalID;
},
metadataModalID: function() {
return this.makeModalName("metadata-modal-");
},
metadataModalTarget: function() {
return "#" + this.metadataModalID;
},
thumbURL: function() {
return `${this.links.download.href}?thumbnail=true`;
},
imgURL: function() {
return this.links.download.href;
},
tagsURL: function() {
return this.links.tags.href;
},
annotationsURL: function() {
return this.links.annotations.href;
},
captureURL: function() {
return this.links.self.href;
},
...mapState("imjoy", { openInImjoyMenuItems: "openImageMenu" })
},
created: function() {},
methods: {
getMetadata: function() {
// Send metadata request
axios
.get(this.captureURL)
.then(response => {
this.$emit("update:tags", response.data.metadata.image.tags);
this.$emit(
"update:annotations",
response.data.metadata.image.annotations
);
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
handleTagSubmit: function(event) {
if (this.newTag !== "") {
this.newTagRequest(this.newTag);
this.newTag = "";
}
UIkit.modal(event.target.parentNode).hide();
},
handleAnnotationsSubmit: function() {
this.putAnnotationsRequest(this.newAnnotations);
this.newAnnotations = {};
},
delCaptureConfirm: function() {
var context = this;
this.modalConfirm("Permanantly delete capture?").then(function() {
context.delCaptureRequest();
});
},
delCaptureRequest: function() {
// Send tag DELETE request
axios
.delete(this.captureURL)
.then(() => {
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
newTagRequest: function(tagString) {
// Send tag PUT request
axios
.put(this.tagsURL, [tagString])
.then(() => {
// Update tag array
this.getTagRequest();
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
putAnnotationsRequest: function(annotationsObject) {
// Send metadata PUT request
axios
.put(this.annotationsURL, annotationsObject)
.then(() => {
// Update metadata object
this.getAnnotationsRequest();
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
delTagConfirm: function(tagString) {
var context = this;
this.modalConfirm(`Remove tag '${tagString}'?`).then(function() {
context.delTagRequest(tagString);
});
},
delTagRequest: function(tagString) {
// Send tag DELETE request
axios
.delete(this.tagsURL, { data: [tagString] })
.then(() => {
// Update tag array
this.getTagRequest();
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
getTagRequest: function() {
// Send tag request
axios
.get(this.tagsURL)
.then(response => {
// Pass the update tag array to the parent
this.$emit("update:tags", response.data);
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
getAnnotationsRequest: function() {
// Send tag request
axios
.get(this.annotationsURL)
.then(response => {
// Pass the update annotation array to the parent
this.$emit("update:annotations", response.data);
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
makeModalName: function(prefix) {
return prefix + this.id;
}
}
};
</script>

View file

@ -1,154 +0,0 @@
<template>
<div
class="capture-card uk-card uk-card-primary uk-padding-remove uk-width-medium"
>
<div class="uk-card-media-top">
<a href="#">
<img
class="uk-width-1-1"
:data-src="thumbnail"
:alt="id"
width="300"
height="225"
uk-img
@click="onClick"
/>
</a>
</div>
<div class="uk-card-body uk-padding-small">
<div
class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right"
uk-grid
>
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand">
<b>{{ type }}: </b>
{{ name }}
</div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto">
<a href="#" class="uk-icon" @click="delAllConfirm()">
<span class="material-symbols-outlined">delete</span>
</a>
</div>
</div>
<div
class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-expand"
>
<time>{{ time }}</time>
</div>
</div>
<div class="uk-card-footer uk-padding-small">
<button
v-if="openInImjoyMenuItems.length != 0"
class="uk-icon"
type="button"
>
<img
style="width:25px;"
src="https://imjoy.io/static/img/imjoy-icon.svg"
/>
</button>
<div uk-dropdown="pos: top-center">
<ul class="uk-nav uk-dropdown-nav">
<li v-for="item in openInImjoyMenuItems" :key="item.name">
<a
href="#"
style="color:black"
@click="item.callback(name, allURLs)"
><span class="material-symbols-outlined">launch</span
>{{ item.title }}</a
>
</li>
</ul>
</div>
&nbsp;
<span
v-for="tag in tags"
:key="tag"
class="uk-label uk-margin-small-right deletable-label"
>
{{ tag }}
</span>
</div>
</div>
</template>
<script>
import axios from "axios";
import { mapState } from "vuex";
// Export main app
export default {
name: "ScanCard",
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
time: {
type: String,
required: true
},
type: {
type: String,
required: false,
default: "Dataset"
},
thumbnail: {
type: String,
required: true
},
captures: {
type: Array,
required: true
},
tags: {
type: Array,
required: false,
default: function() {
return [];
}
}
},
computed: {
allURLs: function() {
var urls = [];
for (var capture of this.captures) {
urls.push(capture.links.self.href);
}
return urls;
},
...mapState("imjoy", { openInImjoyMenuItems: "openScanMenu" })
},
methods: {
onClick: function() {
this.$emit("selectFolder", this.id);
},
delAllConfirm: function() {
var context = this;
this.modalConfirm(
"Permanantly delete all captures in this dataset?"
).then(function() {
context.deleteAll();
});
},
deleteAll: function() {
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
});
}
}
};
</script>

View file

@ -1,163 +0,0 @@
<template>
<div v-if="zipBuilderUri" class="uk-width-small">
<div v-show="downloadReady">
<button
:disabled="isDownloading"
type="button"
class="uk-button uk-button-default uk-width-1-1"
@click="downloadWithAxios()"
>
{{ isDownloading ? `${downloadProgress}%` : "Download" }}
</button>
</div>
<div v-show="!downloadReady">
<taskSubmitter
v-if="zipBuilderUri"
:can-terminate="false"
:submit-url="zipBuilderUri"
:submit-label="'Create ZIP'"
:submit-data="captureIds"
@response="onResponse"
@error="modalError"
>
</taskSubmitter>
</div>
</div>
</template>
<script>
import axios from "axios";
import taskSubmitter from "../../genericComponents/taskSubmitter";
// Export main app
export default {
name: "ZipDownloader",
components: {
taskSubmitter
},
props: {
captureIds: {
type: Array,
required: true
}
},
data: function() {
return {
downloadProgress: 0,
isDownloading: false,
downloadReady: false,
downloadUrl: null,
zipBuilderUri: null,
zipGetterUri: null,
lastSessionId: null
};
},
mounted: function() {
this.updateZipperUri();
},
created: function() {
// Watch for host 'ready', then update status
this.unwatchStoreFunction = this.$store.watch(
(state, getters) => {
return getters.ready;
},
ready => {
if (ready) {
// If the connection is now ready, update zipper URL
this.updateZipperUri();
}
}
);
},
methods: {
updateZipperUri: function() {
if (this.$store.state.available) {
/*axios
.get(this.pluginsUri) // Get a list of plugins
.then(response => {
var plugins = response.data;
var foundExtension = plugins.find(
e => e.title === "org.openflexure.zipbuilder"
);
// if ZipBuilderPlugin is enabled
if (foundExtension) {
// Get plugin action links
this.zipBuilderUri = foundExtension.links.build.href;
this.zipGetterUri = foundExtension.links.get.href;
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});*/
} else {
this.zipBuilderUri = null;
this.zipGetterUri = null;
}
},
resetZipper: function() {
this.downloadReady = false;
},
forceFileDownload(response) {
// Start file download by creating and clicking an invisible link
// One day I hope for a better solution to exist for this...
// A man can dream.....
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `${this.lastSessionId}.zip`); //or any other extension
document.body.appendChild(link);
link.click();
},
downloadWithAxios() {
// Configure progress indicator and response type
let config = {
responseType: "blob",
onDownloadProgress: progressEvent => {
this.downloadProgress = Math.floor(
(progressEvent.loaded * 100) / progressEvent.total
);
}
};
// Set downloading flag
this.isDownloading = true;
// Start download
axios
.get(this.downloadUrl, config)
.then(response => {
this.forceFileDownload(response);
this.deleteLastZip();
this.resetZipper();
})
.catch(error => {
this.modalError(error); // Let mixin handle error
})
.finally(() => {
this.isDownloading = false;
});
},
deleteLastZip() {
axios.delete(this.downloadUrl).catch(error => {
this.modalError(error); // Let mixin handle error
});
},
onResponse: function(response) {
this.lastSessionId = response.output.id;
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
this.downloadReady = true;
}
}
};
</script>

View file

@ -1,455 +0,0 @@
<template>
<div class="galleryDisplay uk-padding uk-padding-remove-top">
<!-- Gallery nav bar -->
<nav
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
uk-navbar="mode: click"
>
<!-- Left side controls -->
<div
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
>
<ul class="uk-navbar-nav">
<li :class="[sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = true">
<span class="material-symbols-outlined">keyboard_arrow_down</span>
</a>
</li>
<li :class="[!sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = false">
<span class="material-symbols-outlined">keyboard_arrow_up</span>
</a>
</li>
<li>
<a href="#">Filter</a>
<div
:class="{
'uk-light uk-background-secondary': $store.state.darkMode
}"
class="uk-navbar-dropdown"
>
<ul class="uk-nav uk-navbar-dropdown-nav">
<form class="uk-form-stacked">
<div
v-for="tag in allTags"
:key="tag"
class="uk-margin-small"
>
<label>
<input
:id="tag"
v-model="checkedTags"
:value="tag"
checked
class="uk-checkbox"
type="checkbox"
/>
{{ tag }}
</label>
</div>
</form>
</ul>
</div>
</li>
</ul>
</div>
<!-- Right side buttons -->
<div class="uk-navbar-right">
<div class="uk-grid">
<div>
<button
class="uk-button uk-button-default uk-width-1-1"
type="button"
@click="updateCaptures()"
>
Refresh Captures
</button>
</div>
<div>
<ZipDownloader :capture-ids="Object.keys(filteredCaptures)" />
</div>
</div>
</div>
</nav>
<!-- Gallery -->
<div
v-if="$store.getters.ready"
class="uk-padding-remove-top"
uk-lightbox="toggle: .lightbox-link"
>
<!-- Folder heading -->
<div
v-if="galleryFolder"
class="gallery-folder-heading uk-flex uk-flex-middle"
>
<a class="uk-icon uk-margin-remove" href="#" @click="galleryBack()">
<span class="material-symbols-outlined">arrow_back</span>
</a>
<div class="uk-margin-left">
<h3 class="uk-margin-remove uk-margin-left">
<b>SCAN</b>
{{ allScans[galleryFolder].name }}
</h3>
</div>
</div>
<!-- Gallery capture cards -->
<div class="gallery-grid">
<div v-for="item in pagedItems" :key="item.id">
<scanCard
v-if="'isScan' in item"
:id="item.id"
:name="item.name"
:time="item.time"
:tags="item.tags"
:type="item.type"
:captures="item.captures"
:thumbnail="item.thumbnail"
@selectFolder="selectFolder"
/>
<captureCard
v-else
:id="item.id"
:links="item.links"
:name="item.name"
:format="item.format"
:path="item.path"
:time="item.time"
:tags="item.tags"
:annotations="item.annotations"
@update:tags="item.tags = $event"
@update:annotations="item.annotations = $event"
/>
</div>
</div>
<Paginate
v-model="page"
:page-count="numberOfPages"
:page-range="3"
:margin-pages="1"
:container-class="'uk-pagination uk-flex-center'"
:prev-text="'Prev'"
:next-text="'Next'"
:page-class="'page-item'"
:active-class="'uk-active'"
:disabled-class="'uk-disabled'"
:click-handler="scrollToTop()"
>
</Paginate>
</div>
</div>
</template>
<script>
import axios from "axios";
import Paginate from "vuejs-paginate";
import captureCard from "./galleryComponents/captureCard.vue";
import scanCard from "./galleryComponents/scanCard.vue";
import ZipDownloader from "./galleryComponents/zipDownloader";
// Export main app
export default {
name: "GalleryDisplay",
components: {
captureCard,
scanCard,
ZipDownloader,
Paginate
},
data: function() {
return {
captures: [],
checkedTags: [],
sortDescending: true,
galleryFolder: "",
scanTag: "scan",
unwatchStoreFunction: null,
maxitems: 10,
page: 1
};
},
computed: {
capturesUri: function() {
return `${this.$store.getters.baseUri}/api/v2/captures`;
},
allTags: function() {
// Return an array of unique tags across all captures
var tags = [];
for (var capture of this.captures) {
for (var tag of capture.tags) {
if (!tags.includes(tag)) {
tags.push(tag);
}
}
}
return tags.sort();
},
noScanCaptures: function() {
// List of captures that are not part of a scan
var captures = [];
for (var capture of this.captures) {
// Add to capture list if matched
if (!this.isDatasetPopulated(capture.dataset)) {
captures.push(capture);
}
}
return captures;
},
allScans: function() {
// List of scans as capture-like objects
var scans = {};
for (var capture of this.captures) {
var dataset = capture.dataset;
if (this.isDatasetPopulated(dataset)) {
var id = dataset["id"];
// If this scan ID hasn't been seen before
if (!(id in scans)) {
scans[id] = {};
scans[id].id = id;
scans[id].name = dataset.name;
scans[id].type = dataset.type;
// Use time of first found capture in dataset
scans[id].time = capture.time;
scans[id].isScan = true;
scans[id].captures = [];
scans[id].tags = [];
}
// Add the capture object to the scan
scans[id].captures.push(capture);
// Append missing tags
for (var tag of capture.tags) {
if (!scans[id].tags.includes(tag)) {
scans[id].tags.push(tag);
}
}
// Create a preview thumbnail
if (!("thumbnail" in scans[id])) {
scans[
id
].thumbnail = `${capture.links.download.href}?thumbnail=true`;
}
}
}
return scans;
},
scanList: function() {
// List of scans, obtained from this.allScans values
return Object.values(this.allScans);
},
itemList: function() {
// Get list of current items to show
// If galleryFolder (ie inside a scan folder), show scan captures
// Otherwise, show root captures and scan cards
if (this.galleryFolder) {
return this.allScans[this.galleryFolder].captures;
} else {
return this.noScanCaptures.concat(this.scanList);
}
},
filteredItems: function() {
// Filter itemList by checkedTags
return this.filterCaptures(this.itemList, this.checkedTags);
},
filteredCaptures: function() {
var captures = {};
for (var item of this.filteredItems) {
// If it's a dataset
if ("captures" in item) {
for (var capture of item.captures) {
// Get the ID of each capture in the set
captures[capture.id] = capture;
}
} else {
// If it's a single capture, get the ID of the capture
captures[item.id] = item;
}
}
return captures;
},
sortedItems: function() {
// Sort filteredItems using sortCaptures function
return this.sortCaptures(this.filteredItems);
},
pagedItems: function() {
let startIndex = (this.page - 1) * this.maxitems;
return this.sortedItems.slice(startIndex, startIndex + this.maxitems);
},
numberOfPages: function() {
return Math.floor(this.sortedItems.length / this.maxitems);
}
},
mounted() {
// Update on mount (does nothing if not connected)
this.updateCaptures();
// A global signal listener to perform a gallery refresh
this.$root.$on("globalUpdateCaptures", () => {
this.updateCaptures();
});
},
created: function() {
// Watch for host 'ready', then update status
this.unwatchStoreFunction = this.$store.watch(
(state, getters) => {
return getters.ready;
},
ready => {
if (ready) {
// If the connection is now ready, update capture list
this.updateCaptures();
} else {
// If the connection is now disconnected, empty capture list
this.captures = {};
}
}
);
},
beforeDestroy() {
// Remove global signal listener to perform a gallery refresh
this.$root.$off("globalUpdateCaptures");
// Then we call that function here to unwatch
if (this.unwatchStoreFunction) {
this.unwatchStoreFunction();
this.unwatchStoreFunction = null;
}
},
methods: {
scrollToTop() {
const el = document.querySelector("#container-left");
if (el) {
el.scrollTop = 0;
}
},
updateCaptures: function() {
if (this.$store.state.available) {
axios
.get(this.capturesUri)
.then(response => {
this.captures = response.data;
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
}
},
isDatasetPopulated: function(dataset) {
if (
!dataset || // If no dataset key
(dataset.constructor === Object && // Or dataset is an object...
Object.keys(dataset).length === 0) // ...but it's empty
) {
return false;
} else {
return true;
}
},
filterCaptures: function(list, filterTags) {
// Filter a list of captures by an array of tags
var result = [];
for (var capture of list) {
// Assume exclusion
var includeCapture = false;
// Filter by selected tags
var tags = capture.tags;
let checker = (arr, target) => target.every(v => arr.includes(v));
// True if all tags match
includeCapture = checker(tags, filterTags);
// Add to capture list if matched
if (includeCapture == true) {
result.push(capture);
}
}
return result;
},
sortCaptures: function(list) {
// Sort a list of captures by metadata time
function compare(a, b) {
if (a.time < b.time) return -1;
if (a.time > b.time) return 1;
return 0;
}
if (this.sortDescending == true) {
return list.sort(compare).reverse();
} else {
return list.sort(compare);
}
},
galleryBack: function() {
this.galleryFolder = "";
this.page = 1;
},
selectFolder: function(folderID) {
this.galleryFolder = folderID;
}
}
};
</script>
<style lang="less" scoped>
.gallery-navbar {
border-width: 0 0 1px 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.gallery-navbar,
.gallery-folder-heading {
margin-bottom: 30px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 320px);
justify-content: center;
overflow-y: auto;
}
.gallery-grid > div {
display: inline-block;
margin-bottom: 20px;
}
/deep/ .capture-card {
width: 300px;
height: 100%; // Used to have all cards in a row match their heights
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -68,16 +68,18 @@
</div>
</div>
<taskSubmitter
:submit-url="zeroActionUri"
:submit-label="'Zero Coordinates'"
<action-button
thing="stage"
action="set_zero_position"
submit-label="Zero Coordinates"
:can-terminate="false"
@finished="updatePosition"
@error="modalError"
></taskSubmitter>
<taskSubmitter
:submit-url="recentreActionUri"
:submit-label="'Re-centre Stage'"
/>
<action-button
thing="auto_recentre_stage"
action="recentre"
submit-label="Re-centre Stage"
:can-terminate="true"
:requires-confirmation="true"
:modal-progress="true"
@ -86,7 +88,7 @@
"
@finished="updatePosition"
@error="modalError"
></taskSubmitter>
/>
</div>
</li>
@ -109,9 +111,10 @@
</a>
</div>
<p>
<taskSubmitter
ref="moveTaskSubmitter"
:submit-url="absoluteMoveUri"
<action-button
ref="moveButton"
thing="stage"
action="move_absolute"
:submit-data="setPosition"
:submit-label="'Move'"
:can-terminate="true"
@ -122,7 +125,7 @@
moveLock = false;
"
@error="modalError"
></taskSubmitter>
/>
</p>
</form>
</div>
@ -134,9 +137,9 @@
<div class="uk-accordion-content">
<div class="uk-grid-small uk-child-width-expand" uk-grid>
<div v-show="!isAutofocusing || isAutofocusing == 1">
<taskSubmitter
v-if="fastAutofocusUri"
:submit-url="fastAutofocusUri"
<action-button
thing="autofocus"
action="fast_autofocus"
:submit-data="{ dz: 2000 }"
:submit-label="'Autofocus'"
:button-primary="false"
@ -147,36 +150,36 @@
isAutofocusing = 0;
"
@error="modalError"
></taskSubmitter>
/>
</div>
</div>
</div>
</li>
<li v-show="captureUri" class="uk-open">
<li class="uk-open">
<a class="uk-accordion-title" href="#">Image Capture</a>
<div class="uk-accordion-content">
<div class="uk-margin">
<taskSubmitter
v-if="captureUri"
:submit-url="captureUri"
<action-button
thing="camera"
action="capture_jpeg"
:submit-data="{ resolution: 'main' }"
:submit-label="'Low Resolution'"
:submit-on-event="'globalCaptureEvent'"
@response="handleCaptureResponse"
@error="modalError"
></taskSubmitter>
/>
</div>
<div class="uk-margin">
<taskSubmitter
v-if="captureUri"
:submit-url="captureUri"
<action-button
thing="camera"
action="capture_jpeg"
:submit-data="{ resolution: 'full' }"
submit-label="Full Resolution"
submit-on-event="globalCaptureEvent"
:submit-on-event="'globalCaptureEvent'"
@response="handleCaptureResponse"
@error="modalError"
></taskSubmitter>
/>
</div>
</div>
</li>
@ -194,14 +197,14 @@
<script>
import axios from "axios";
import taskSubmitter from "../../genericComponents/taskSubmitter";
import ActionButton from "../../labThingsComponents/actionButton.vue";
// Export main app
export default {
name: "PaneNavigate",
components: {
taskSubmitter
ActionButton
},
data: function() {
@ -228,24 +231,9 @@ export default {
baseUri: function() {
return this.$store.getters.baseUri;
},
absoluteMoveUri: function() {
return this.thingActionUrl("stage", "move_absolute");
},
zeroActionUri: function() {
return this.thingActionUrl("stage", "set_zero_position");
},
recentreActionUri: function() {
return this.thingActionUrl("auto_recentre_stage", "recentre");
},
positionStatusUri: function() {
return `${this.baseUri}/stage/position`;
},
fastAutofocusUri: function() {
return this.thingActionUrl("autofocus", "fast_autofocus");
},
captureUri: function() {
return this.thingActionUrl("camera", "capture_jpeg");
},
moveInImageCoordinatesUri: function() {
return this.thingActionUrl(
"camera_stage_mapping",
@ -327,7 +315,7 @@ export default {
await this.startMoveTask();
},
async startMoveTask() {
await this.$refs.moveTaskSubmitter.startTask();
await this.$refs.moveButton.startTask();
},
moveInImageCoordinatesRequest: function(x, y) {
// If not movement-locked
@ -359,18 +347,6 @@ export default {
}
},
zeroRequest: function() {
// Send move request
axios
.post(this.zeroActionUri)
.then(() => {
this.updatePosition(); // Update the position in text boxes
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
async updatePosition() {
this.setPosition = await this.readThingProperty("stage", "position");
},

View file

@ -1,8 +1,8 @@
<template>
<div
<div
v-observe-visibility="visibilityChanged"
class="galleryDisplay uk-padding uk-padding-remove-top"
>
>
<!-- Gallery nav bar -->
<nav
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
@ -45,24 +45,26 @@
<div class="uk-card">
<div class="uk-card-body">
<h3 class="uk-card-title">{{ item.name }}</h3>
<task-submitter
<action-button
thing="smart_scan"
action="create_zip_of_scan"
submit-label="Download ZIP"
:can-terminate="false"
:submit-data="{'scan_name': item.name}"
:submit-data="{ scan_name: item.name }"
:button-primary="true"
:submit-url="createZipOfScanUri"
@response="downloadZipFile"
@error="modalError"
/>
<button class="uk-button" @click="deleteScan(item.name)">
Delete
</button>
<task-submitter
<action-button
submit-label="Stitch Images"
thing="smart_scan"
action="stitch_scan"
:can-terminate="false"
:submit-data="{'scan_name': item.name}"
:submit-data="{ scan_name: item.name }"
:button-primary="false"
:submit-url="stitchUri"
:modal-progress="true"
@error="modalError"
/>
@ -87,12 +89,12 @@
<script>
import axios from "axios";
import taskSubmitter from '../genericComponents/taskSubmitter.vue';
import actionButton from "../labThingsComponents/actionButton.vue";
// Export main app
export default {
components: { taskSubmitter },
name: "ScanListContent",
components: { actionButton },
data: function() {
return {
@ -108,12 +110,6 @@ export default {
"readproperty",
true
);
},
createZipOfScanUri() {
return this.thingActionUrl("smart_scan", "create_zip_of_scan");
},
stitchUri() {
return this.thingActionUrl("smart_scan", "stitch_scan");
}
},
@ -162,7 +158,7 @@ export default {
},
async updateScans() {
let scans = await this.readThingProperty("smart_scan", "scans");
if (!scans | scans.length == 0) {
if (!scans | (scans.length == 0)) {
this.scans = scans;
}
scans.forEach(scan => {
@ -216,7 +212,7 @@ export default {
},
async downloadZipFile(response) {
const scan_name = response.input.scan_name;
const filename = `${scan_name}_images.zip`
const filename = `${scan_name}_images.zip`;
const url = response.output.href;
const link = document.createElement("a");
link.href = url;

View file

@ -2,14 +2,15 @@
<div id="CSMCalibrationSettings">
<!--Show auto calibrate if default plugin is enabled-->
<div v-if="'calibrate_xy' in actions" class="uk-margin-small">
<taskSubmitter
<action-button
:button-primary="true"
:can-terminate="true"
:requires-confirmation="true"
:confirmation-message="
'Start recalibration of the stage to the camera? This may take a while, and the microscope will be locked during this time.'
"
:submit-url="calibrateXYUri"
thing="camera_stage_mapper"
action="calibrate_xy"
:submit-label="'Auto-Calibrate using camera'"
:modal-progress="true"
@response="onRecalibrateResponse"
@ -29,14 +30,14 @@
</template>
<script>
import taskSubmitter from "../../../genericComponents/taskSubmitter";
import ActionButton from "../../../labThingsComponents/actionButton.vue";
// Export main app
export default {
name: "CSMCalibrationSettings",
components: {
taskSubmitter
ActionButton
},
props: {
@ -55,9 +56,6 @@ export default {
properties() {
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping")
.properties;
},
calibrateXYUri() {
return this.thingActionUrl("camera_stage_mapping", "calibrate_xy");
}
},

View file

@ -2,55 +2,55 @@
<div>
<!--Show auto calibrate if default plugin is enabled-->
<div v-if="'full_auto_calibrate' in actions" class="uk-margin-small">
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="true"
:confirmation-message="
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
"
:submit-url="cameraUri + 'full_auto_calibrate'"
thing="camera"
action="full_auto_calibrate"
:submit-label="'Full Auto-Calibrate'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
<div v-if="'auto_expose_from_minimum' in actions" class="uk-margin-small">
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="false"
:submit-url="cameraUri + 'auto_expose_from_minimum'"
thing="camera"
action="auto_expose_from_minimum"
:submit-label="'Auto gain &amp; shutter speed'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
<div v-if="'calibrate_white_balance' in actions" class="uk-margin-small">
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="false"
:submit-url="cameraUri + 'calibrate_white_balance'"
thing="camera"
action="calibrate_white_balance"
:submit-label="'Auto white balance'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
<div v-if="'calibrate_lens_shading' in actions" class="uk-margin-small">
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="true"
:confirmation-message="
'Is the microscope looking at an evenly illuminated, empty field of view? ' +
'If not, the current image will show through in any images captured afterwards.'
"
:submit-url="cameraUri + 'calibrate_lens_shading'"
thing="camera"
action="calibrate_lens_shading"
:submit-label="'Auto flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
<div
@ -58,44 +58,43 @@
v-if="'flatten_lens_shading_table' in actions"
class="uk-child-width-expand"
>
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="false"
:submit-url="cameraUri + 'flat_lens_shading'"
thing="camera"
action="flat_lens_shading"
:submit-label="'Disable flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
<div
v-show="showExtraSettings"
v-if="'reset_lens_shading' in actions"
class="uk-child-width-expand"
>
<taskSubmitter
<action-button
:can-terminate="false"
:requires-confirmation="false"
:submit-url="cameraUri + 'reset_lens_shading'"
thing="camera"
action="reset_lens_shading"
:submit-label="'Reset flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
/>
</div>
</div>
</template>
<script>
import axios from "axios";
import taskSubmitter from "../../../genericComponents/taskSubmitter";
import ActionButton from "../../../labThingsComponents/actionButton.vue";
// Export main app
export default {
name: "CameraCalibrationSettings",
components: {
taskSubmitter
ActionButton
},
props: {
@ -110,38 +109,15 @@ export default {
}
},
data: function() {
return {
actions: {},
isCalibrating: false,
LstDownloadEnabled: false
};
},
mounted() {
this.updateActions();
computed: {
actions() {
return this.$store.getters["wot/thingDescription"]("camera").actions;
}
},
methods: {
updateActions: async function() {
try {
let response = await axios.get(this.cameraUri); // Get the thing description
let td = response.data;
this.actions = td.actions;
} catch (error) {
this.modalError(error); // Let mixin handle error
}
},
onRecalibrateResponse: function() {
this.modalNotify("Finished recalibration.");
},
flattenLensShadingTableRequest: function() {
axios.post(this.recalibrationLinks.flatten_lens_shading_table.href);
},
deleteLensShadingTableRequest: function() {
axios.post(this.recalibrationLinks.delete_lens_shading_table.href);
}
}
};

View file

@ -71,8 +71,9 @@
</tabIcon>
</li>
</ul>
<task-submitter
:submit-url="saveAllThingSettingsUri"
<action-button
thing="settings"
action="save_all_thing_settings"
submit-label="Save all settings"
class="uk-margin"
/>
@ -142,7 +143,7 @@ import stageSettings from "./settingsComponents/stageSettings.vue";
// Import generic components
import tabIcon from "../genericComponents/tabIcon";
import tabContent from "../genericComponents/tabContent";
import TaskSubmitter from '../genericComponents/taskSubmitter.vue';
import ActionButton from "../labThingsComponents/actionButton.vue";
// Export main app
export default {
@ -157,7 +158,7 @@ export default {
featuresSettings,
tabIcon,
tabContent,
TaskSubmitter
ActionButton
},
data: function() {
@ -167,12 +168,6 @@ export default {
};
},
computed: {
saveAllThingSettingsUri() {
return this.thingActionUrl("settings", "save_all_thing_settings");
}
},
methods: {
setTab: function(event, tab) {
if (!(this.currentTab == tab)) {

View file

@ -31,7 +31,7 @@
label="Image overlap (0-1)"
/>
</div>
<div class="uk-margin">
<div class="uk-margin">
<propertyControl
thing-name="smart_scan"
property-name="stitch_tiff"
@ -70,9 +70,10 @@
/>
</div>
<div class="uk-margin">
<taskSubmitter
ref="smartScanTaskSubmitter"
:submit-url="smartScanUri"
<action-button
ref="smartScanButton"
thing="smart_scan"
action="sample_scan"
:submit-data="{ scan_name: scan_name }"
submit-label="Start smart scan"
:can-terminate="true"
@ -84,9 +85,9 @@
</div>
</div>
<div v-show="scanning">
<h2 v-if="displayImageOnRight">
Live stitching preview
</h2>
<h2 v-if="displayImageOnRight">
Live stitching preview
</h2>
<mini-stream-display v-if="displayImageOnRight" />
<action-log-display
id="log-display"
@ -98,36 +99,47 @@
v-if="cancellable"
type="button"
class="uk-button uk-button-danger uk-width-1-1"
@click="$refs.smartScanTaskSubmitter.terminateTask()"
@click="$refs.smartScanButton.terminateTask()"
>
Cancel
</button>
<div class="uk-margin uk-grid-small uk-child-width-expand" v-if="!cancellable" uk-grid>
<button
type="button"
class="uk-button"
@click="scanning = false; lastStitchedImage=null;"
<div
v-if="!cancellable"
class="uk-margin uk-grid-small uk-child-width-expand"
uk-grid
>
Close
</button>
<task-submitter
class="uk-button"
submit-label="Download ZIP"
:can-terminate="false"
:submit-data="{'scan_name': lastScanName}"
:button-primary="true"
:submit-url="createZipOfScanUri"
@response="downloadZipFile"
@error="modalError"
/>
<button
type="button"
class="uk-button"
@click="
scanning = false;
lastStitchedImage = null;
"
>
Close
</button>
<action-button
class="uk-button"
thing="smart_scan"
action="create_zip_of_scan"
submit-label="Download ZIP"
:can-terminate="false"
:submit-data="{ scan_name: lastScanName }"
:button-primary="true"
@response="downloadZipFile"
@error="modalError"
/>
</div>
</div>
<h3 v-if="scanning">
Scan ID: {{ lastScanName }}
</h3>
<h3 v-if="scanning">Scan ID: {{ lastScanName }}</h3>
</div>
<div class="view-image uk-width-expand uk-height-1-1">
<img v-if="displayImageOnRight" class=image-fit :src="lastStitchedImage" id="last-stitched-image">
<img
v-if="displayImageOnRight"
id="last-stitched-image"
class="image-fit"
:src="lastStitchedImage"
/>
<streamDisplay v-else />
</div>
</div>
@ -135,22 +147,22 @@
<script>
import streamDisplay from "./streamContent.vue";
import taskSubmitter from "../genericComponents/taskSubmitter";
import propertyControl from "../labThingsComponents/propertyControl.vue";
import actionLogDisplay from "../genericComponents/actionLogDisplay.vue";
import actionProgressBar from "../genericComponents/actionProgressBar.vue";
import MiniStreamDisplay from '../genericComponents/miniStreamDisplay.vue';
import actionLogDisplay from "../labThingsComponents/actionLogDisplay.vue";
import actionProgressBar from "../labThingsComponents/actionProgressBar.vue";
import MiniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
import ActionButton from "../labThingsComponents/actionButton.vue";
export default {
name: "SlideScanContent",
components: {
streamDisplay,
taskSubmitter,
propertyControl,
actionLogDisplay,
actionProgressBar,
MiniStreamDisplay
MiniStreamDisplay,
ActionButton
},
data() {
@ -168,20 +180,14 @@ export default {
},
computed: {
createZipOfScanUri() {
return this.thingActionUrl("smart_scan", "create_zip_of_scan");
},
backendOK() {
return this.thingAvailable("smart_scan");
},
smartScanUri() {
return this.thingActionUrl("smart_scan", "sample_scan");
},
cancellable() {
return (this.taskStatus == "running") | (this.taskStatus == "pending");
},
displayImageOnRight() {
return this.scanning & this.lastStitchedImage !== null;
return this.scanning & (this.lastStitchedImage !== null);
}
},
@ -190,27 +196,32 @@ export default {
this.scanRunning = false;
this.modalError(error);
},
correlateCurrentScan() {
},
correlateCurrentScan() {},
startScanning() {
this.lastStitchedImage = null;
this.scanning = true;
setTimeout(this.pollScan, 1000);
},
async pollScan() {
if (this.cancellable) { // while the scan is running
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time");
if (this.cancellable) {
// while the scan is running
let mtime = await this.readThingProperty(
"smart_scan",
"latest_preview_stitch_time"
);
if (mtime !== null) {
this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
}
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name");
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
this.lastScanName = await this.readThingProperty(
"smart_scan",
"latest_scan_name"
);
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
}
},
async downloadZipFile(response) {
const scan_name = response.input.scan_name;
const filename = `${scan_name}_images.zip`
const filename = `${scan_name}_images.zip`;
const url = response.output.href;
const link = document.createElement("a");
link.href = url;

View file

@ -66,12 +66,12 @@ Vue.mixin({
// `false` fails because axios somehow eats it!
// Other values should not be stringified or pydantic
// can't parse them.
if (value===false | value===true) {
value=JSON.stringify(value);
if ((value === false) | (value === true)) {
value = JSON.stringify(value);
}
await axios.put(url, value);
},
thingActionUrl(thing, action, allow_missing=false) {
thingActionUrl(thing, action, allow_missing = false) {
let url = this.$store.getters["wot/thingActionUrl"](
thing,
action,