Merge branch 'action-button' into 'v3'
Replace tasksubmitter with action-button See merge request openflexure/openflexure-microscope-server!176
This commit is contained in:
commit
61d94e205d
23 changed files with 229 additions and 2339 deletions
|
|
@ -429,7 +429,7 @@ html {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.image-fit{
|
||||
.image-fit {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -316,16 +253,6 @@ export default {
|
|||
...mapState({ imjoyEnabled: "imjoyEnabled" })
|
||||
},
|
||||
|
||||
created: function() {
|
||||
if (this.$store.getters.ready) {
|
||||
// Update plugins
|
||||
this.updatePlugins().then(() => {
|
||||
// Start initialisation modals
|
||||
this.startModals();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// A global signal listener to switch tab
|
||||
this.$root.$on("globalSwitchTab", tabID => {
|
||||
|
|
@ -339,22 +266,12 @@ export default {
|
|||
this.$root.$on("globalDecrementTab", () => {
|
||||
this.incrementTabBy(-1);
|
||||
});
|
||||
if (this.$store.getters.ready) {
|
||||
this.startModals();
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
|
@ -369,7 +286,7 @@ export default {
|
|||
this.currentTab = newId;
|
||||
},
|
||||
startModals: function() {
|
||||
this.$refs["calibrationModal"].show();
|
||||
this.$refs.calibrationModal.show();
|
||||
},
|
||||
enterApp: function() {
|
||||
// Stuff to do once connected and all init modals are finished
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -160,13 +160,6 @@ export default {
|
|||
miniStreamDisplay
|
||||
},
|
||||
|
||||
props: {
|
||||
availablePlugins: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
ready: false,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,21 +125,21 @@
|
|||
moveLock = false;
|
||||
"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
/>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!--Show autofocus if default plugin is enabled-->
|
||||
<li v-show="fastAutofocusUri || normalAutofocusUri" class="uk-open">
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Autofocus</a>
|
||||
<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");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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_mapping"
|
||||
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");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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 & 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue