Rename js directory to webapp
This commit is contained in:
parent
375bcf4e73
commit
b2eb1e0f05
75 changed files with 5 additions and 5 deletions
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<form
|
||||
class="uk-form-stacked"
|
||||
action=""
|
||||
method="GET"
|
||||
@submit="overrideAPIHost"
|
||||
>
|
||||
<label class="uk-form-label">Override API origin</label>
|
||||
<input
|
||||
v-model="newOrigin"
|
||||
name="overrideOrigin"
|
||||
class="uk-input"
|
||||
type="text"
|
||||
/>
|
||||
<label class="uk-form-label">
|
||||
<input
|
||||
v-model="reloadWhenOverridingOrigin"
|
||||
class="uk-input uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Reload web app with new origin
|
||||
</label>
|
||||
<button class="uk-button uk-button-default uk-margin-small">
|
||||
Apply
|
||||
</button>
|
||||
</form>
|
||||
<form class="uk-form-stacked" @submit.prevent="resetTour">
|
||||
<label class="uk-form-label">Re-run tour on next load</label>
|
||||
<button class="uk-button uk-button-default uk-margin-small">
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "DevTools",
|
||||
|
||||
components: {},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
newOrigin: this.$store.state.origin,
|
||||
reloadWhenOverridingOrigin: true
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (localStorage.overrideOrigin) {
|
||||
this.newOrigin = localStorage.overrideOrigin;
|
||||
} else {
|
||||
this.newOrigin = "http://microscope.local:5000";
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
overrideAPIHost: function(event) {
|
||||
// Save the origin override, so that if we reload the web app, you can easily
|
||||
localStorage.overrideOrigin = this.newOrigin;
|
||||
|
||||
// If we have elected not to reload the interface, just update the origin
|
||||
// in the store. Otherwise, the form's default action will do the job for us.
|
||||
// TODO: preserve other query parameters when reloading
|
||||
if (!this.reloadWhenOverridingOrigin) {
|
||||
this.$store.commit("changeOrigin", this.newOrigin);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
resetTour: function() {
|
||||
// Make the introduction tour run next time the app loads
|
||||
this.setLocalStorageObj("completedTour", false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.error-icon {
|
||||
font-size: 120px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<div class="host-input">
|
||||
<div v-if="configuration">
|
||||
<div>
|
||||
<div class="uk-margin-small-bottom">
|
||||
<b>API origin:</b>
|
||||
<br />
|
||||
{{ $store.state.origin }}
|
||||
<br />
|
||||
<b>API URL:</b>
|
||||
<br />
|
||||
{{ $store.getters.uriV2 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div v-if="settings">
|
||||
<b>Device name:</b> <br />
|
||||
{{ settings.name }}
|
||||
</div>
|
||||
<div>
|
||||
<b>Server version:</b> <br />
|
||||
{{ configuration.application.version }}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="uk-margin-small-bottom">
|
||||
<b>Camera:</b>
|
||||
<br />
|
||||
<div v-if="configuration.camera.type != 'MissingCamera'">
|
||||
{{ configuration.camera.type }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No camera connected</b></div>
|
||||
</div>
|
||||
<div>
|
||||
<b>Stage:</b>
|
||||
<br />
|
||||
<div v-if="configuration.stage.type != 'MissingStage'">
|
||||
{{ configuration.stage.type }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No stage connected</b></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="uk-grid-small uk-child-width-1-2" uk-grid>
|
||||
<div>
|
||||
<button
|
||||
v-show="'shutdown' in systemActionLinks"
|
||||
class="uk-button uk-button-danger uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
|
||||
@click="shutdownRequest"
|
||||
>
|
||||
Shutdown
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
v-show="'reboot' in systemActionLinks"
|
||||
class="uk-button uk-button-danger uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
|
||||
@click="rebootRequest"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="$store.state.waiting">
|
||||
Loading...
|
||||
</div>
|
||||
<div v-else-if="$store.state.error">
|
||||
<b>Error:</b> {{ $store.state.error }}
|
||||
</div>
|
||||
<div v-else>No active connection</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "StatusPane",
|
||||
|
||||
components: {},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
configuration: null,
|
||||
settings: null,
|
||||
systemActionLinks: {}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
configurationUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/configuration`;
|
||||
},
|
||||
rootUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
// Watch for host 'ready', then update configuration
|
||||
this.updateConfiguration();
|
||||
this.updateSettings();
|
||||
this.updateSystemActions();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateConfiguration: function() {
|
||||
axios
|
||||
.get(this.configurationUri)
|
||||
.then(response => {
|
||||
this.configuration = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
updateSystemActions: function() {
|
||||
axios
|
||||
.get(this.rootUri)
|
||||
.then(response => {
|
||||
if ("RebootAPI" in response.data.actions) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"reboot",
|
||||
`${this.$store.getters.baseUri}/api/v2/actions/system/reboot/`
|
||||
);
|
||||
} else {
|
||||
delete this.systemActionLinks.reboot;
|
||||
}
|
||||
if ("ShutdownAPI" in response.data.actions) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"shutdown",
|
||||
`${this.$store.getters.baseUri}/api/v2/actions/system/shutdown/`
|
||||
);
|
||||
} else {
|
||||
delete this.systemActionLinks.shutdown;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
shutdownRequest: function() {
|
||||
this.modalConfirm("Shut down microscope?").then(
|
||||
() => {
|
||||
if ("shutdown" in this.systemActionLinks) {
|
||||
this.$store.commit("resetState");
|
||||
// Post and silence errors
|
||||
axios.post(this.systemActionLinks.shutdown).catch(() => {});
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
rebootRequest: function() {
|
||||
this.modalConfirm("Restart microscope?").then(
|
||||
() => {
|
||||
if ("reboot" in this.systemActionLinks) {
|
||||
this.$store.commit("resetState");
|
||||
// Post and silence errors
|
||||
axios.post(this.systemActionLinks.reboot).catch(() => {});
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="less"></style>
|
||||
35
webapp/src/components/tabContentComponents/aboutContent.vue
Normal file
35
webapp/src/components/tabContentComponents/aboutContent.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div class="uk-height-1-1 view-component">
|
||||
<div class="uk-padding-small">
|
||||
<statusPane class="uk-width-large" />
|
||||
</div>
|
||||
<div class="uk-padding-small">
|
||||
<h2>Links</h2>
|
||||
<a
|
||||
class="uk-link"
|
||||
target="_blank"
|
||||
href="https://gitlab.com/openflexure/openflexure-microscope-server/-/issues"
|
||||
>Report an issue</a
|
||||
>
|
||||
</div>
|
||||
<div class="uk-padding-small">
|
||||
<h2>Developer tools</h2>
|
||||
<devTools class="uk-width-large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import devTools from "./aboutComponents/devTools.vue";
|
||||
import statusPane from "./aboutComponents/statusPane.vue";
|
||||
|
||||
export default {
|
||||
name: "AboutContent",
|
||||
|
||||
components: {
|
||||
devTools,
|
||||
statusPane
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,562 @@
|
|||
<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="scanUri" :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 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="scanUri" :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"
|
||||
},
|
||||
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.
|
||||
scanUri: null,
|
||||
smartScanUri: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
resizeClass: function() {
|
||||
return {
|
||||
"uk-disabled": !this.resizeCapture
|
||||
};
|
||||
},
|
||||
captureActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/capture`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
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"
|
||||
};
|
||||
},
|
||||
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() {
|
||||
this.updateScanUri();
|
||||
// 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
|
||||
});
|
||||
},
|
||||
|
||||
updateScanUri: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.scan"
|
||||
);
|
||||
// if ScanPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.scanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.smart-stack"
|
||||
);
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.smartScanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.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>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="control-component">
|
||||
<paneCapture />
|
||||
</div>
|
||||
<div class="view-component uk-width-expand">
|
||||
<streamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import paneCapture from "./captureComponents/paneCapture";
|
||||
import streamDisplay from "./streamContent.vue";
|
||||
|
||||
export default {
|
||||
name: "CaptureContent",
|
||||
|
||||
components: {
|
||||
paneCapture,
|
||||
streamDisplay
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="view-component uk-width-expand">
|
||||
<galleryContent v-if="viewPanel == 'gallery'" />
|
||||
<settingsContent v-else-if="viewPanel == 'settings'" />
|
||||
<streamDisplay v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsonForm from "../pluginComponents/JsonForm";
|
||||
import streamDisplay from "./streamContent.vue";
|
||||
import galleryContent from "../tabContentComponents/galleryContent.vue";
|
||||
import settingsContent from "../tabContentComponents/settingsContent.vue";
|
||||
|
||||
export default {
|
||||
name: "ExtensionContent",
|
||||
|
||||
components: {
|
||||
JsonForm,
|
||||
streamDisplay,
|
||||
galleryContent,
|
||||
settingsContent
|
||||
},
|
||||
|
||||
props: {
|
||||
forms: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
frame: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
viewPanel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "stream"
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.vue-friendly-iframe {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vue-friendly-iframe iframe {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
<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()">
|
||||
<i class="material-icons">delete</i>
|
||||
</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)"
|
||||
><i class="material-icons">launch</i>{{ 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"><i class="material-icons">label</i></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>
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<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()">
|
||||
<i class="material-icons">delete</i>
|
||||
</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)"
|
||||
><i class="material-icons">launch</i>{{ 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>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<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
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
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>
|
||||
455
webapp/src/components/tabContentComponents/galleryContent.vue
Normal file
455
webapp/src/components/tabContentComponents/galleryContent.vue
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
<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">
|
||||
<i class="material-icons">keyboard_arrow_down</i>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="[!sortDescending ? 'uk-active' : '']">
|
||||
<a class="uk-icon" href="#" @click="sortDescending = false">
|
||||
<i class="material-icons">keyboard_arrow_up</i>
|
||||
</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()">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</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>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div
|
||||
class="uk-card uk-card-hover uk-card-small uk-card-default uk-margin"
|
||||
:class="{ 'uk-card-secondary': $store.state.darkMode }"
|
||||
>
|
||||
<div class="uk-card-body">
|
||||
<div class="uk-grid-small uk-flex-middle" uk-grid>
|
||||
<div class="uk-width-auto">
|
||||
<img
|
||||
v-if="iconURL"
|
||||
style="height:50px"
|
||||
:data-src="iconURL"
|
||||
:alt="name + ' icon'"
|
||||
uk-img
|
||||
/>
|
||||
<i v-if="!iconURL" class="material-icons" style="font-size:50px">
|
||||
{{ iconName }}
|
||||
</i>
|
||||
</div>
|
||||
<div class="uk-width-expand">
|
||||
<h3 class="uk-card-title uk-margin-remove-bottom">{{ name }}</h3>
|
||||
<p class="uk-text-meta uk-margin-remove-top">
|
||||
<slot name="subtitle"></slot>
|
||||
</p>
|
||||
</div>
|
||||
<div class="uk-width-auto">
|
||||
<button class="uk-button" @click="$emit('click')">
|
||||
<slot name="buttonText">Launch</slot>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ImjoyPluginCard",
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
iconURL: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => ""
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => "extension"
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
644
webapp/src/components/tabContentComponents/imjoyContent.vue
Normal file
644
webapp/src/components/tabContentComponents/imjoyContent.vue
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div class="uk-height-1-1 view-component">
|
||||
<progress
|
||||
ref="imjoyProgressbar"
|
||||
style="height: 4px; margin-bottom:0!important"
|
||||
class="uk-progress"
|
||||
max="100"
|
||||
></progress>
|
||||
|
||||
<div
|
||||
class="uk-flex uk-flex-column uk-margin uk-margin-left uk-margin-right"
|
||||
>
|
||||
<imjoy-plugin-card
|
||||
name="Load From URL"
|
||||
icon-u-r-l="https://imjoy.io/static/img/imjoy-icon.svg"
|
||||
@click="loadPluginDialog()"
|
||||
>
|
||||
<template v-slot:buttonText>Open</template>
|
||||
</imjoy-plugin-card>
|
||||
<imjoy-plugin-card
|
||||
name="ImageJ.JS"
|
||||
icon-u-r-l="https://ij.imjoy.io/assets/icons/chrome/chrome-installprocess-128-128.png"
|
||||
@click="openImageJ()"
|
||||
/>
|
||||
<imjoy-plugin-card
|
||||
name="Kaibu"
|
||||
icon-u-r-l="https://kaibu.org/static/img/kaibu-icon.svg"
|
||||
@click="startKaibu()"
|
||||
/>
|
||||
<imjoy-plugin-card
|
||||
v-for="(p, name) in loadedPlugins"
|
||||
:key="p.id"
|
||||
:name="name"
|
||||
icon-name="extension"
|
||||
@click="runPlugin(p)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="loading" class="progress uk-margin-small">
|
||||
<div class="indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as imjoyCore from "imjoy-core";
|
||||
import axios from "axios";
|
||||
import UIkit from "uikit";
|
||||
import { mapState } from "vuex";
|
||||
import ImjoyPluginCard from "./imjoyComponents/imjoyPluginCard.vue";
|
||||
|
||||
// TODO: move this to a module or something...
|
||||
async function pollAction(response) {
|
||||
// Poll an action until its status is completed
|
||||
// TODO: raise an error if it fails or is cancelled
|
||||
// For ease, this accepts and returns a response object from
|
||||
// axios.get
|
||||
// FIXME: this could be an infinite loop if the task is cancelled/errored
|
||||
let r = response;
|
||||
while ((r.data.status == "running") | (r.data.status == "pending")) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
r = await axios.get(r.data.href);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Image element and wait for it to load.
|
||||
*
|
||||
* This returns a Promise, so you can "await" the image
|
||||
* element rather than needing to add a callback.
|
||||
*/
|
||||
function loadImageFromUrl(src) {
|
||||
// Create an Image, and return it once it has loaded
|
||||
// see https://stackoverflow.com/a/66180709
|
||||
// no need to declare async, as it returns a Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
// We must set crossOrigin="anonymous" in order
|
||||
// to avoid security errors when we get the pixel
|
||||
// data
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an image from a URL, and return the RGB data
|
||||
* in a format that can be cast to an ndarray
|
||||
*
|
||||
* This follows a slightly convoluted recipe, where we
|
||||
* "draw" the image, and then extract the pixels from the
|
||||
* 2D canvas.
|
||||
*/
|
||||
async function imageUrlToNdarray(url) {
|
||||
// see https://stackoverflow.com/questions/934012/get-image-data-url-in-javascript
|
||||
const img = await loadImageFromUrl(url);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const rgbaData = imageData.data; // This gets the RGBA values as an ArrayBuffer
|
||||
// convert RGBA to RGB
|
||||
const rgbData = new Uint8Array(new ArrayBuffer(img.height * img.width * 3));
|
||||
for (let i = 0; i < img.height; i++) {
|
||||
for (let j = 0; j < img.width; j++) {
|
||||
const pos = i * img.width + j;
|
||||
rgbData[pos * 3] = rgbaData[pos * 4];
|
||||
rgbData[pos * 3 + 1] = rgbaData[pos * 4 + 1];
|
||||
rgbData[pos * 3 + 2] = rgbaData[pos * 4 + 2];
|
||||
}
|
||||
}
|
||||
return {
|
||||
_rtype: "ndarray",
|
||||
_rdtype: "uint8",
|
||||
_rshape: [img.height, img.width, 3],
|
||||
_rvalue: rgbData.buffer
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "ImJoyContent",
|
||||
|
||||
components: { ImjoyPluginCard },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadedPlugins: {},
|
||||
viewers: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
captureActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/capture`;
|
||||
},
|
||||
snapshotStreamUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/streams/snapshot`;
|
||||
},
|
||||
getPositionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/state/stage/position`;
|
||||
},
|
||||
moveActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/stage/move`;
|
||||
},
|
||||
baseUri: function() {
|
||||
return this.$store.getters.baseUri;
|
||||
},
|
||||
...mapState("imjoy", { windows: "tabs" })
|
||||
},
|
||||
mounted() {
|
||||
const self = this;
|
||||
const imjoy = new imjoyCore.ImJoy({
|
||||
// We will load the ImJoy plugin base frame via http (instead of https)
|
||||
// such that imjoy plugins can access insecured urls
|
||||
// however, some browser features won't work (e.g. webrtc)
|
||||
// unless we explicitly set `base_frame` in the plugin to
|
||||
// https://lib.imjoy.io/default_base_frame.html
|
||||
default_base_frame: "http://lib.imjoy.io/default_base_frame.html",
|
||||
imjoy_api: {
|
||||
async showDialog(plugin, config, exta_config) {
|
||||
return await imjoy.pm.createWindow(plugin, config, exta_config);
|
||||
},
|
||||
async showSnackbar(plugin, msg, duration) {
|
||||
duration = duration || 5;
|
||||
UIkit.notification.closeAll();
|
||||
UIkit.notification({
|
||||
message: msg.slice(0, 100),
|
||||
status: "primary",
|
||||
pos: "bottom-center",
|
||||
timeout: duration * 1000
|
||||
});
|
||||
},
|
||||
async showMessage(plugin, msg) {
|
||||
imjoy.imjoy_api.showSnackbar(plugin, msg, 5);
|
||||
},
|
||||
async showStatus(plugin, status) {
|
||||
imjoy.imjoy_api.showSnackbar(plugin, status, 5);
|
||||
},
|
||||
async showProgress(plugin, p) {
|
||||
p = p || 0;
|
||||
if (p < 1.0) p = p * 100;
|
||||
if (p > 100) p = 100;
|
||||
if (p) {
|
||||
self.$refs.imjoyProgressbar.style.display = "block";
|
||||
self.$refs.imjoyProgressbar.value = parseInt(p);
|
||||
} else {
|
||||
self.$refs.imjoyProgressbar.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
imjoy.start({ workspace: "default" }).then(async () => {
|
||||
console.log("ImJoy started");
|
||||
imjoy.event_bus.on("add_window", w => {
|
||||
this.addWindow(w);
|
||||
});
|
||||
this.setupMicroscopeAPI();
|
||||
this.setupViewers();
|
||||
const origin = window.location.origin;
|
||||
const localPlugins = [
|
||||
"OpenFlexureSnapImageTemplate",
|
||||
"OpenFlexureScriptEditor",
|
||||
"OpenFlexureTestMoveStage",
|
||||
"ListServices"
|
||||
];
|
||||
for (let fname of localPlugins) {
|
||||
await this.loadPlugin(`${origin}/${fname}.imjoy.html`);
|
||||
}
|
||||
this.setupPluginEngine();
|
||||
});
|
||||
this.imjoy = imjoy;
|
||||
},
|
||||
methods: {
|
||||
setupPluginEngine() {
|
||||
// setup a plugin engine for Python plugins
|
||||
this.imjoy.pm
|
||||
.reloadPluginRecursively({
|
||||
uri:
|
||||
"https://imjoy-team.github.io/jupyter-engine-manager/Jupyter-Engine-Manager.imjoy.html"
|
||||
})
|
||||
.then(enginePlugin => {
|
||||
// TODO: currently the url query are not preserved for ImJoy
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const engine = urlParams.get("engine");
|
||||
const spec = urlParams.get("spec");
|
||||
if (engine) {
|
||||
enginePlugin.api
|
||||
.createEngine({
|
||||
name: "MyCustomEngine",
|
||||
nbUrl: engine,
|
||||
url: engine.split("?")[0]
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Jupyter Engine connected!");
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("Failed to connect to Jupyter Engine", e);
|
||||
});
|
||||
} else {
|
||||
enginePlugin.api
|
||||
.createEngine({
|
||||
name: "MyBinder Engine",
|
||||
url: "https://mybinder.org",
|
||||
spec: spec || "oeway/imjoy-binder-image/master"
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Binder Engine connected!");
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("Failed to connect to MyBinder Engine", e);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
async setupMicroscopeAPI() {
|
||||
// Here we register a set of API for controlling the microscope as a service
|
||||
// For interoperatability, it might be good to reuse a subset of the function names defined in MicroManager
|
||||
// See here: https://valelab4.ucsf.edu/~MM/doc/MMCore/html/class_c_m_m_core.html
|
||||
const snapshotUri = this.snapshotStreamUri;
|
||||
const captureActionUri = this.captureActionUri;
|
||||
const modalError = this.modalError;
|
||||
const getPositionAsObject = this.getStagePosition;
|
||||
const setPositionAsObject = this.setStagePosition;
|
||||
const getInstrumentSettings = this.getInstrumentSettings;
|
||||
const setInstrumentSettings = this.setInstrumentSettings;
|
||||
const runAutofocus = this.runAutofocus;
|
||||
|
||||
const service = {
|
||||
_rintf: true, // this will make sure the function can be called multiple times
|
||||
type: "#microscope-control",
|
||||
name: "OpenFlexure",
|
||||
lastSnapResponse: null,
|
||||
snapPreviewImage() {
|
||||
// The "proper" capture method is more involved - this one pulls a frame out of the preview stream
|
||||
return axios
|
||||
.get(snapshotUri)
|
||||
.then(response => {
|
||||
return response.data;
|
||||
})
|
||||
.catch(modalError);
|
||||
},
|
||||
snapImage() {
|
||||
// Do capture
|
||||
return axios.post(captureActionUri, {}).then(response => {
|
||||
service.lastSnapResponse = response;
|
||||
// TODO: send the events to flash the stream and update captures
|
||||
// Flash the stream (capture animation)
|
||||
//this.$root.$emit("globalFlashStream");
|
||||
// Update the global capture list
|
||||
//this.$root.$emit("globalUpdateCaptures");
|
||||
});
|
||||
//.catch(modalError);
|
||||
},
|
||||
async getImage() {
|
||||
// FIXME: handle nicely getImage() being called before snapImage()
|
||||
// Wait until the capture has completed and we can retrieve it
|
||||
service.lastSnapResponse = await pollAction(service.lastSnapResponse);
|
||||
let capture = service.lastSnapResponse.data.output;
|
||||
// TODO: check links.download.href exists and is JPEG
|
||||
let jpeg = await axios
|
||||
.get(capture.links.download.href, {
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
"Content-Type": "image/jpeg"
|
||||
}
|
||||
})
|
||||
.then(r => r.data);
|
||||
console.log(
|
||||
"Downloaded ",
|
||||
jpeg.byteLength,
|
||||
" bytes of JPEG image data"
|
||||
);
|
||||
return jpeg; //TODO: what is the expected output format???
|
||||
},
|
||||
setPosition(z) {
|
||||
// The C api suggests this defaults to just Z, and can accept a "label" to
|
||||
// select the stage.
|
||||
return setPositionAsObject({
|
||||
z: z,
|
||||
absolute: true
|
||||
});
|
||||
},
|
||||
async getPosition() {
|
||||
const { z } = await getPositionAsObject();
|
||||
return z;
|
||||
},
|
||||
async setXYPosition(x, y) {
|
||||
return setPositionAsObject({
|
||||
x: x,
|
||||
y: y,
|
||||
absolute: true
|
||||
});
|
||||
},
|
||||
async getXYPosition() {
|
||||
const { x, y } = await getPositionAsObject();
|
||||
return [x, y];
|
||||
},
|
||||
async getXYZPosition() {
|
||||
const { x, y, z } = await getPositionAsObject();
|
||||
return [x, y, z];
|
||||
},
|
||||
async setXYZPosition(x, y, z) {
|
||||
return setPositionAsObject({
|
||||
x: x,
|
||||
y: y,
|
||||
z: z,
|
||||
absolute: true
|
||||
});
|
||||
},
|
||||
async getExposure() {
|
||||
const settings = await getInstrumentSettings();
|
||||
if (settings.camera.picamera) {
|
||||
if (settings.camera.picamera.shutter_speed) {
|
||||
return settings.camera.picamera.shutter_speed / 1000;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
async setExposure(exposure) {
|
||||
setInstrumentSettings({
|
||||
camera: {
|
||||
picamera: {
|
||||
shutter_speed: exposure * 1000
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
async fullFocus() {
|
||||
return runAutofocus();
|
||||
}
|
||||
};
|
||||
const ref = await this.imjoy.pm.registerService(
|
||||
{ type: "#microscope-control", name: "OpenFlexure" },
|
||||
service
|
||||
);
|
||||
console.log("registered service");
|
||||
console.log(ref);
|
||||
},
|
||||
closeWindow(w) {
|
||||
if (w) {
|
||||
w.api.close();
|
||||
this.$store.commit("imjoy/removeTab", w);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Activate a window - you can pass the window object, its name, or its id.
|
||||
*/
|
||||
selectWindow(window) {
|
||||
for (let w of this.windows) {
|
||||
if ((w === window) | (w.name === window) | (w.id === window)) {
|
||||
// Make the relevant window visible by switching to its tab
|
||||
this.$root.$emit("globalSwitchTab", `ImJoy-Plugin-${w.id}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
setWindowIconUrl(window, iconURL) {
|
||||
this.$store.commit("imjoy/setTabProperty", {
|
||||
tab: window,
|
||||
key: "iconURL",
|
||||
value: iconURL
|
||||
});
|
||||
},
|
||||
async startKaibu() {
|
||||
this.viewers.kaibu = await this.imjoy.pm.createWindow(null, {
|
||||
name: "Kaibu",
|
||||
src: "https://kaibu.org/#/app"
|
||||
});
|
||||
this.viewers.kaibu.on("close", () => {
|
||||
delete this.viewers.kaibu;
|
||||
});
|
||||
this.setWindowIconUrl(
|
||||
this.viewers.kaibu.config.id,
|
||||
"https://kaibu.org/static/img/kaibu-icon.svg"
|
||||
);
|
||||
this.viewers.kaibu.set_mode("full");
|
||||
},
|
||||
async startImageJ() {
|
||||
this.viewers.imagej = await this.imjoy.pm.createWindow(null, {
|
||||
name: "ImageJ.JS",
|
||||
src: "https://ij.imjoy.io"
|
||||
});
|
||||
this.viewers.imagej.on("close", () => {
|
||||
delete this.viewers.imagej;
|
||||
});
|
||||
this.setWindowIconUrl(
|
||||
this.viewers.imagej.config.id,
|
||||
"https://ij.imjoy.io/assets/icons/chrome/chrome-installprocess-128-128.png"
|
||||
);
|
||||
// load the ITK/VTK viewer for imagej.js
|
||||
this.loadPlugin(
|
||||
"https://gist.githubusercontent.com/oeway/e5c980fbf6582f25fde795262a7e33ec/raw/itk-vtk-viewer-imagej.imjoy.html"
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Switch to the ImageJ window, starting it if necessary.
|
||||
*/
|
||||
async openImageJ() {
|
||||
if (!this.viewers.imagej) {
|
||||
await this.startImageJ();
|
||||
}
|
||||
this.selectWindow("ImageJ.JS");
|
||||
return this.viewers.imagej;
|
||||
},
|
||||
/**
|
||||
* Switch to the Kaibu window, starting it if necessary.
|
||||
*/
|
||||
async openKaibu() {
|
||||
if (!this.viewers.kaibu) {
|
||||
await this.startKaibu();
|
||||
}
|
||||
this.selectWindow("Kaibu"); // TODO: make this behave more nicely when we have multiple Kaibu windows
|
||||
return this.viewers.kaibu;
|
||||
},
|
||||
async setupViewers() {
|
||||
this.loading = true;
|
||||
// TODO: improve this using the ImJoy `api.getServices`
|
||||
// See here: https://imjoy.io/docs/#/api?id=apigetservices
|
||||
// The idea is that plugins can register custom services via `api.registerService`
|
||||
// For example, we can have a image visualization service
|
||||
// Then other plugins can use `api.getServices` to get a list of services
|
||||
// And this can be used for render our "Open In ImJoy" menu
|
||||
try {
|
||||
const self = this;
|
||||
|
||||
this.$store.commit("imjoy/addOpenImageItem", {
|
||||
title: "Kaibu",
|
||||
async callback(name, imageUrl) {
|
||||
const viewer = await self.openKaibu();
|
||||
await viewer.view_image(imageUrl, { type: "2d-image", name });
|
||||
await viewer.add_shapes([]);
|
||||
}
|
||||
});
|
||||
|
||||
this.$store.commit("imjoy/addOpenImageItem", {
|
||||
title: "ImageJ.JS",
|
||||
async callback(name, imageUrl) {
|
||||
const viewer = await self.openImageJ();
|
||||
const response = await fetch(imageUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
await viewer.viewImage(buffer, {
|
||||
name: name.replace(".jpeg", ".jpg")
|
||||
});
|
||||
}
|
||||
});
|
||||
this.$store.commit("imjoy/addOpenImageItem", {
|
||||
title: "ImageJ.JS [ndarray]",
|
||||
async callback(name, imageUrl) {
|
||||
const viewer = await self.openImageJ();
|
||||
const ndarray = await imageUrlToNdarray(imageUrl);
|
||||
await viewer.viewImage(ndarray, {
|
||||
name: name.replace(".jpeg", "")
|
||||
});
|
||||
}
|
||||
});
|
||||
this.$store.commit("imjoy/addOpenScanItem", {
|
||||
title: "ImageJ.JS [stack]",
|
||||
async callback(name, allURLs) {
|
||||
const viewer = await self.openImageJ();
|
||||
// allURLs is a list of URLs for *capture objects*, so
|
||||
// first we need to retrieve them, then extract image URLs
|
||||
// and finally turn image URLs into "numpy arrays" (RGB data).
|
||||
const imagesAsNdarrays = await Promise.all(
|
||||
allURLs.map(async url => {
|
||||
const r = await axios.get(url);
|
||||
return await imageUrlToNdarray(r.data.links.download.href);
|
||||
})
|
||||
);
|
||||
// Construct an array to hold all the images, based on the first image
|
||||
// TODO: assert that _rdtype is uint8, _rshape is the same, _rtype is ndarray
|
||||
const shape = imagesAsNdarrays[0]._rshape;
|
||||
const totalSize = imagesAsNdarrays.reduce(
|
||||
(total, image) => total + image._rvalue.byteLength,
|
||||
0
|
||||
);
|
||||
const stackData = new Uint8Array(new ArrayBuffer(totalSize));
|
||||
const transferredBytes = imagesAsNdarrays.reduce((total, image) => {
|
||||
console.log(`inserting image at ${total} bytes`);
|
||||
// TODO: is it bad to populate stackData as a "side effect"?
|
||||
stackData.set(new Uint8Array(image._rvalue), total);
|
||||
//assert image._rshape == shape;
|
||||
return total + image._rvalue.byteLength;
|
||||
}, 0);
|
||||
const stackNdarray = {
|
||||
_rtype: "ndarray",
|
||||
_rdtype: "uint8",
|
||||
_rshape: [imagesAsNdarrays.length, shape[0], shape[1], shape[2]],
|
||||
_rvalue: stackData.buffer
|
||||
};
|
||||
console.log(
|
||||
`Stack is ${transferredBytes} and will have shape: ${stackNdarray._rshape}`
|
||||
);
|
||||
await viewer.viewImage(stackNdarray, {
|
||||
name
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
alert(`Failed to setup viewers, error: ${e}`);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async addWindow(w) {
|
||||
// Here we can add check w.standalone if we want to allow the plugin to decide whether to open in a new tab.
|
||||
this.$store.commit("imjoy/addTab", w);
|
||||
await this.$nextTick();
|
||||
//this.$forceUpdate();
|
||||
this.selectWindow(w);
|
||||
},
|
||||
loadPlugin(uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.loading = true;
|
||||
this.imjoy.pm
|
||||
.reloadPluginRecursively({
|
||||
uri
|
||||
})
|
||||
.then(async plugin => {
|
||||
this.loadedPlugins[plugin.name] = plugin;
|
||||
this.loading = false;
|
||||
resolve(plugin);
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
this.loading = false;
|
||||
reject(e);
|
||||
alert(`failed to load the plugin, error: ${e}`);
|
||||
});
|
||||
});
|
||||
},
|
||||
async runPlugin(plugin) {
|
||||
await plugin.api.run();
|
||||
},
|
||||
loadPluginDialog() {
|
||||
const uri = prompt(
|
||||
"Paste the ImJoy plugin url here",
|
||||
"imjoy-team/imjoy-plugins:Welcome"
|
||||
);
|
||||
if (uri) {
|
||||
this.loadPlugin(uri);
|
||||
}
|
||||
},
|
||||
async getStagePosition() {
|
||||
return axios.get(this.getPositionUri).then(r => r.data);
|
||||
},
|
||||
async setStagePosition(payload) {
|
||||
return axios.post(this.moveActionUri, payload).then(pollAction);
|
||||
},
|
||||
async getInstrumentSettings() {
|
||||
return axios
|
||||
.get(`${this.baseUri}/api/v2/instrument/settings/`)
|
||||
.then(r => r.data);
|
||||
},
|
||||
async setInstrumentSettings(payload) {
|
||||
return axios.put(`${this.baseUri}/api/v2/instrument/settings/`, payload);
|
||||
},
|
||||
runAutofocus() {
|
||||
// This mechanism is also used by the keybinding for autofocus.
|
||||
// It means we don't have a way to poll, but that's not a problem.
|
||||
this.$root.$emit("globalFastAutofocusEvent");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.window-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.imjoy-container-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
height: 26px;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
background-color: #efefef;
|
||||
color: #482727;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plugins-button {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
height: 26px;
|
||||
line-height: 11px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
right: 100px;
|
||||
height: 26px;
|
||||
}
|
||||
</style>
|
||||
179
webapp/src/components/tabContentComponents/loggingContent.vue
Normal file
179
webapp/src/components/tabContentComponents/loggingContent.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<div
|
||||
v-observe-visibility="visibilityChanged"
|
||||
class="uk-padding uk-padding-remove-top"
|
||||
>
|
||||
<!-- Logging nav bar -->
|
||||
<nav
|
||||
class="logging-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"
|
||||
>
|
||||
<select v-model="filterLevel" class="uk-select">
|
||||
<option v-for="level in allLevels" :key="level">{{ level }}</option>
|
||||
</select>
|
||||
</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="updateLogs()"
|
||||
>
|
||||
Refresh Logs
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<a class="uk-button uk-button-default" :href="logFileURI" download
|
||||
>Download Log File</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Logging items -->
|
||||
<div class="uk-width-xlarge uk-align-center">
|
||||
<div
|
||||
v-for="item in pagedItems"
|
||||
:key="item.timestamp"
|
||||
uk-alert
|
||||
class="logging-entry"
|
||||
:class="{
|
||||
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
|
||||
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
|
||||
}"
|
||||
>
|
||||
<b>{{ formatDateTime(item.data.created) }}</b>
|
||||
<div class="logging-message">{{ formatMessage(item) }}</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";
|
||||
|
||||
export default {
|
||||
name: "LoggingContent",
|
||||
|
||||
components: {
|
||||
Paginate
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
maxitems: 20,
|
||||
page: 1,
|
||||
logs: [],
|
||||
allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
filterLevel: "WARNING"
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredLevels: function() {
|
||||
let cutoffIndex = this.allLevels.indexOf(this.filterLevel);
|
||||
return this.allLevels.slice(cutoffIndex, -1);
|
||||
},
|
||||
filteredItems: function() {
|
||||
var items = [];
|
||||
for (var item of this.logs) {
|
||||
// Add to capture list if matched
|
||||
if (this.filteredLevels.includes(item.data.levelname)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
loggingUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/events/logging`;
|
||||
},
|
||||
logFileURI: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/log`;
|
||||
},
|
||||
pagedItems: function() {
|
||||
let startIndex = (this.page - 1) * this.maxitems;
|
||||
return this.filteredItems.slice(startIndex, startIndex + this.maxitems);
|
||||
},
|
||||
numberOfPages: function() {
|
||||
return Math.floor(this.filteredItems.length / this.maxitems);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Update on mount (does nothing if not connected)
|
||||
this.updateLogs();
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
const el = document.querySelector("#container-left");
|
||||
if (el) {
|
||||
el.scrollTop = 0;
|
||||
}
|
||||
},
|
||||
visibilityChanged(isVisible) {
|
||||
if (isVisible) {
|
||||
this.updateLogs();
|
||||
}
|
||||
},
|
||||
updateLogs: function() {
|
||||
axios
|
||||
.get(this.loggingUri)
|
||||
.then(response => {
|
||||
this.logs = response.data.reverse();
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
formatDateTime: function(isoDateTimeString) {
|
||||
let date = new Date(isoDateTimeString);
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||
},
|
||||
formatMessage: function(item) {
|
||||
return item.data.levelname + ": " + item.data.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logging-navbar {
|
||||
border-width: 0 0 1px 0;
|
||||
border-style: solid;
|
||||
border-color: rgba(180, 180, 180, 0.25);
|
||||
margin-bottom: 30px;
|
||||
height: 80px;
|
||||
}
|
||||
.logging-entry {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
.logging-message {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
<template>
|
||||
<div id="paneNavigate" class="uk-padding-small">
|
||||
<div v-if="setPosition">
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Configure</a>
|
||||
<div class="uk-accordion-content">
|
||||
<b>STEP SIZE</b>
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="stepSize.x"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputStepXy"
|
||||
/>
|
||||
</div>
|
||||
<label class="uk-margin-small-right"
|
||||
><input
|
||||
v-model="invert.x"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Invert x</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="stepSize.y"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputStepY"
|
||||
/>
|
||||
</div>
|
||||
<label class="uk-margin-small-right"
|
||||
><input
|
||||
v-model="invert.y"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Invert y</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="stepSize.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputStepZz"
|
||||
/>
|
||||
</div>
|
||||
<label
|
||||
><input
|
||||
v-model="invert.z"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Invert z</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="uk-button uk-button-default uk-margin uk-width-1-1"
|
||||
@click="zeroRequest()"
|
||||
>
|
||||
Zero coordinates
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Move-to</a>
|
||||
<div class="uk-accordion-content">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<!-- Text boxes to set and view position -->
|
||||
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.x"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionX"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.y"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionZx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-default uk-float-right uk-width-1-1"
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!--Show autofocus if default plugin is enabled-->
|
||||
<li v-show="fastAutofocusUri || normalAutofocusUri" 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"
|
||||
:submit-data="{ dz: 2000 }"
|
||||
:submit-label="'Fast'"
|
||||
:button-primary="false"
|
||||
:submit-on-event="'globalFastAutofocusEvent'"
|
||||
@taskStarted="isAutofocusing = 1"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="!isAutofocusing || isAutofocusing == 2">
|
||||
<taskSubmitter
|
||||
v-if="normalAutofocusUri"
|
||||
:submit-url="normalAutofocusUri"
|
||||
:submit-data="{ dz: [-90, -60, -30, 0, 30, 60, 90] }"
|
||||
:submit-label="'Medium'"
|
||||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 2"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="!isAutofocusing || isAutofocusing == 3">
|
||||
<taskSubmitter
|
||||
v-if="normalAutofocusUri"
|
||||
:submit-url="normalAutofocusUri"
|
||||
:submit-data="{ dz: [-30, -20, -10, 0, 10, 20, 30] }"
|
||||
:submit-label="'Fine'"
|
||||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 3"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="uk-text-warning">
|
||||
<p>Stage positioning is disabled since no stage is connected.</p>
|
||||
<p>
|
||||
Please check all data and power connections to your motor controller
|
||||
board.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "PaneNavigate",
|
||||
|
||||
components: {
|
||||
taskSubmitter
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
stepXy: 200,
|
||||
stepZz: 50,
|
||||
stepSize: {
|
||||
x: 200,
|
||||
y: 200,
|
||||
z: 50
|
||||
},
|
||||
invert: {
|
||||
x: false,
|
||||
y: false,
|
||||
z: false
|
||||
},
|
||||
setPosition: null,
|
||||
isAutofocusing: 0,
|
||||
moveLock: false,
|
||||
fastAutofocusUri: null,
|
||||
normalAutofocusUri: null,
|
||||
moveInImageCoordinatesUri: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
moveActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/stage/move`;
|
||||
},
|
||||
zeroActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/stage/zero`;
|
||||
},
|
||||
positionStatusUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/state/stage/position`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
stepSize: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.setLocalStorageObj("navigation_stepSize", this.stepSize);
|
||||
}
|
||||
},
|
||||
invert: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.setLocalStorageObj("navigation_invert", this.invert);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Reload saved settings
|
||||
this.stepSize =
|
||||
this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
|
||||
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
|
||||
// A global signal listener to perform a move action
|
||||
this.$root.$on("globalMoveEvent", (x, y, z, absolute) => {
|
||||
this.moveRequest(x, y, z, absolute);
|
||||
});
|
||||
// A global signal listener to perform a move action in pixels
|
||||
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
||||
this.moveInImageCoordinatesRequest(x, y, absolute);
|
||||
});
|
||||
// A global signal listener to perform a move in multiples of a step size
|
||||
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
||||
this.moveRequest(
|
||||
x_steps * this.stepSize.x * (this.invert.x ? -1 : 1),
|
||||
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
|
||||
z_steps * this.stepSize.z * (this.invert.z ? -1 : 1),
|
||||
false
|
||||
);
|
||||
});
|
||||
// Update the current position in text boxes
|
||||
this.updatePosition();
|
||||
// Look for autofocus plugin
|
||||
this.updateAutofocusUri();
|
||||
this.updateMoveInImageCoordinatesUri();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// Remove global signal listener to perform a move action
|
||||
this.$root.$off("globalMoveEvent");
|
||||
this.$root.$off("globalMoveInImageCoordinatesEvent");
|
||||
this.$root.$off("globalMoveStepEvent");
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSubmit: function() {
|
||||
this.moveRequest(
|
||||
this.setPosition.x,
|
||||
this.setPosition.y,
|
||||
this.setPosition.z,
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
moveRequest: function(x, y, z, absolute) {
|
||||
// If not movement-locked
|
||||
if (!this.moveLock) {
|
||||
// Lock move requests
|
||||
this.moveLock = true;
|
||||
|
||||
// Send move request
|
||||
axios
|
||||
.post(this.moveActionUri, {
|
||||
x: x,
|
||||
y: y,
|
||||
z: z,
|
||||
absolute: absolute
|
||||
})
|
||||
.then(() => {
|
||||
this.updatePosition(); // Update the position in text boxes
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
})
|
||||
.then(() => {
|
||||
this.moveLock = false; // Release the move lock
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
moveInImageCoordinatesRequest: function(x, y) {
|
||||
// If not movement-locked
|
||||
if (!this.moveLock) {
|
||||
// Lock move requests
|
||||
this.moveLock = true;
|
||||
|
||||
if (!this.moveInImageCoordinatesUri) {
|
||||
this.modalError(
|
||||
"Moving in image coordinates is not supported - you will need to upgrade your microscope's software."
|
||||
);
|
||||
}
|
||||
|
||||
// Send move request
|
||||
axios
|
||||
.post(this.moveInImageCoordinatesUri, {
|
||||
x: y, // NB the coordinates are numpy/PIL style, meaning X and Y are swapped.
|
||||
y: x
|
||||
})
|
||||
.then(() => {
|
||||
this.updatePosition(); // Update the position in text boxes
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
})
|
||||
.then(() => {
|
||||
this.moveLock = false; // Release the move lock
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
});
|
||||
},
|
||||
|
||||
updatePosition: function() {
|
||||
axios
|
||||
.get(this.positionStatusUri)
|
||||
.then(response => {
|
||||
this.setPosition = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateAutofocusUri: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.autofocus"
|
||||
);
|
||||
// if ScanPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.fastAutofocusUri = foundExtension.links.fast_autofocus.href;
|
||||
this.normalAutofocusUri = foundExtension.links.autofocus.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateMoveInImageCoordinatesUri: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.moveInImageCoordinatesUri =
|
||||
foundExtension.links.move_in_image_coordinates.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="control-component">
|
||||
<paneNavigate />
|
||||
</div>
|
||||
<div class="view-component uk-width-expand">
|
||||
<streamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import paneNavigate from "./navigateComponents/paneNavigate";
|
||||
import streamDisplay from "./streamContent.vue";
|
||||
|
||||
export default {
|
||||
name: "NavigateContent",
|
||||
|
||||
components: {
|
||||
paneNavigate,
|
||||
streamDisplay
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div
|
||||
id="CSMSettings"
|
||||
class="uk-grid uk-grid-divider uk-child-width-expand"
|
||||
uk-grid
|
||||
>
|
||||
<div class="uk-width-large">
|
||||
<h3>Camera/stage mapping</h3>
|
||||
<p>
|
||||
Camera/stage mapping allows the stage to move relative to the camera
|
||||
view. This enables functions like click-to-move, and more precise tile
|
||||
scans.
|
||||
</p>
|
||||
<CSMCalibrationSettings />
|
||||
</div>
|
||||
<div id="mini-stream">
|
||||
<miniStreamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CSMSettings",
|
||||
|
||||
components: {
|
||||
CSMCalibrationSettings,
|
||||
miniStreamDisplay
|
||||
},
|
||||
|
||||
props: {
|
||||
showExtraSettings: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null,
|
||||
recalibrationLinks: {},
|
||||
isCalibrating: false,
|
||||
dataAvailable: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
this.updateRecalibrationLinks();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
// Update links
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings =
|
||||
response.data.extensions["org.openflexure.camera_stage_mapping"];
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
this.settings = {};
|
||||
});
|
||||
},
|
||||
|
||||
updateCalibrationDataAvailability: function() {
|
||||
if ("get_calibration" in this.recalibrationLinks) {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (Object.keys(response.data).length === 0) {
|
||||
this.dataAvailable = false;
|
||||
} else {
|
||||
this.dataAvailable = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getCalibrationData: function() {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (response.data != {}) {
|
||||
const data = JSON.stringify(response.data);
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "csm_calibration.json");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
// if camera-stage mapping extension is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
// Update whether calibration data is available
|
||||
this.updateCalibrationDataAvailability();
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished stage-to-camera calibration.");
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
onRecalibrateError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.center-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
#mini-stream {
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div id="CSMCalibrationSettings">
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<div v-if="'calibrate_xy' in recalibrationLinks" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:button-primary="true"
|
||||
:can-terminate="false"
|
||||
: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="recalibrationLinks.calibrate_xy.href"
|
||||
:submit-label="'Auto-Calibrate using camera'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<button
|
||||
v-if="'get_calibration' in recalibrationLinks"
|
||||
v-show="dataAvailable && showExtraSettings"
|
||||
type="button"
|
||||
class="uk-button uk-button-default uk-width-1-1"
|
||||
@click="getCalibrationData()"
|
||||
>
|
||||
Download calibration data
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../../genericComponents/taskSubmitter";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CSMCalibrationSettings",
|
||||
|
||||
components: {
|
||||
taskSubmitter
|
||||
},
|
||||
|
||||
props: {
|
||||
showExtraSettings: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null,
|
||||
recalibrationLinks: {},
|
||||
isCalibrating: false,
|
||||
dataAvailable: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
this.updateRecalibrationLinks();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
// Update links
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings =
|
||||
response.data.extensions["org.openflexure.camera_stage_mapping"];
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
this.settings = {};
|
||||
});
|
||||
},
|
||||
|
||||
updateCalibrationDataAvailability: function() {
|
||||
if ("get_calibration" in this.recalibrationLinks) {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (Object.keys(response.data).length === 0) {
|
||||
this.dataAvailable = false;
|
||||
} else {
|
||||
this.dataAvailable = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getCalibrationData: function() {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (response.data != {}) {
|
||||
const data = JSON.stringify(response.data);
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "csm_calibration.json");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
// if camera-stage mapping extension is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
// Update whether calibration data is available
|
||||
this.updateCalibrationDataAvailability();
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished stage-to-camera calibration.");
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.center-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div id="appSettings" class="uk-width-large">
|
||||
<h3>Appearance</h3>
|
||||
<p>
|
||||
<label>
|
||||
Theme
|
||||
<select v-model="appTheme" class="uk-select">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "AppSettings",
|
||||
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
appTheme: {
|
||||
get() {
|
||||
return this.$store.state.appTheme;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeAppTheme", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
appTheme: function() {
|
||||
this.setLocalStorageObj("appTheme", this.appTheme);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Try loading settings from localStorage. If null, don't change.
|
||||
this.appTheme = this.getLocalStorageObj("appTheme") || this.appTheme;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
<template>
|
||||
<div id="cameraSettings">
|
||||
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
|
||||
<div class="uk-width-large">
|
||||
<h3>Manual camera settings</h3>
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div v-if="picamera.shutter_speed !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Exposure time</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.shutter_speed"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.analog_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Analogue gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.analog_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.digital_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Digital gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.digital_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.awb_gains !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text">
|
||||
White Balance gains
|
||||
</label>
|
||||
<div class="uk-form-controls">
|
||||
<label class="uk-form-label">R:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[0]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
<label class="uk-form-label">B:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[1]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Image Quality</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
<div v-if="jpeg_quality !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>JPEG capture quality (%)</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="jpeg_quality"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="stream_resolution !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Stream resolution</label
|
||||
>
|
||||
<select
|
||||
v-model="stream_resolution"
|
||||
class="uk-select uk-form-small"
|
||||
>
|
||||
<option
|
||||
v-for="option in resolutionOptions"
|
||||
:key="option.value[0]"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Advanced</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div v-if="mjpeg_bitrate !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Camera bitrate</label
|
||||
>
|
||||
<select
|
||||
v-model="mjpeg_bitrate"
|
||||
class="uk-select uk-form-small"
|
||||
>
|
||||
<option
|
||||
v-for="option in bitrateOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="uk-margin-remove">
|
||||
This option sets the target bitrate for camera
|
||||
recordings.<br />
|
||||
<b
|
||||
>Limiting bitrate may impact fast-autofocus
|
||||
reliability.</b
|
||||
>
|
||||
However is guaranteed to fix the live stream disappearing
|
||||
for some highly detailed samples.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="picamera.framerate !== undefined"
|
||||
class="uk-margin-top"
|
||||
>
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Camera framerate</label
|
||||
>
|
||||
<select
|
||||
v-model="picamera.framerate"
|
||||
class="uk-select uk-form-small"
|
||||
>
|
||||
<option
|
||||
v-for="option in framerateOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="uk-margin-remove">
|
||||
This option sets the framerate of the Pi Camera video
|
||||
encoder. The actual stream framerate is determined by
|
||||
network speed, and is limited by the encoder framerate.<br />
|
||||
<b
|
||||
>Lowering framerate may impact fast-autofocus
|
||||
accuracy.</b
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<h3>Automatic calibration</h3>
|
||||
<cameraCalibrationSettings></cameraCalibrationSettings>
|
||||
</div>
|
||||
|
||||
<div id="mini-stream">
|
||||
<miniStreamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CameraSettings",
|
||||
|
||||
components: {
|
||||
cameraCalibrationSettings,
|
||||
miniStreamDisplay
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
picamera: {
|
||||
shutter_speed: undefined,
|
||||
analog_gain: undefined,
|
||||
digital_gain: undefined,
|
||||
framerate: undefined,
|
||||
awb_gains: undefined
|
||||
},
|
||||
mjpeg_bitrate: undefined,
|
||||
stream_resolution: undefined,
|
||||
jpeg_quality: undefined,
|
||||
bitrateOptions: [
|
||||
{ text: "Maximum (unlimited)", value: -1 },
|
||||
{ text: "High (25Mbps)", value: 25000000 },
|
||||
{ text: "Normal (17Mbps)", value: 17000000 },
|
||||
{ text: "Low (5Mbps)", value: 5000000 },
|
||||
{ text: "Very low (2.5Mbps)", value: 2500000 }
|
||||
],
|
||||
resolutionOptions: [
|
||||
{ text: "Higher (832, 624)", value: [832, 624] },
|
||||
{ text: "Normal (640, 480)", value: [640, 480] }
|
||||
],
|
||||
framerateOptions: [
|
||||
{ text: "Normal (30fps)", value: 30 },
|
||||
{ text: "Low (15fps)", value: 15 },
|
||||
{ text: "Very low (10fps)", value: 10 }
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
const cameraSettings = response.data.camera;
|
||||
// Get base camera settings
|
||||
this.mjpeg_bitrate = cameraSettings.mjpeg_bitrate;
|
||||
this.jpeg_quality = cameraSettings.jpeg_quality;
|
||||
this.stream_resolution = cameraSettings.stream_resolution;
|
||||
// Get Pi Camera settings if they exist
|
||||
if (cameraSettings.picamera) {
|
||||
this.picamera.analog_gain = cameraSettings.picamera.analog_gain;
|
||||
this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
|
||||
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
|
||||
this.picamera.framerate = cameraSettings.picamera.framerate;
|
||||
this.picamera.awb_gains = cameraSettings.picamera.awb_gains;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
applySettingsRequest: function() {
|
||||
// We have to use parseInt/parseFloat because JS sometimes seems to
|
||||
// make the numbers be strings... TypeScript would solve this...
|
||||
var payload = {
|
||||
camera: {
|
||||
mjpeg_bitrate: parseInt(this.mjpeg_bitrate),
|
||||
jpeg_quality: parseInt(this.jpeg_quality),
|
||||
stream_resolution: this.stream_resolution,
|
||||
picamera: {
|
||||
shutter_speed: parseFloat(this.picamera.shutter_speed),
|
||||
analog_gain: parseFloat(this.picamera.analog_gain),
|
||||
digital_gain: parseFloat(this.picamera.digital_gain),
|
||||
framerate: parseInt(this.picamera.framerate),
|
||||
awb_gains: [
|
||||
parseFloat(this.picamera.awb_gains[0]),
|
||||
parseFloat(this.picamera.awb_gains[1])
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log(payload);
|
||||
// Send request
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
return new Promise(r => setTimeout(r, 500));
|
||||
}) // why is there no built-in for this??!
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Camera settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
#mini-stream {
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<div>
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<div v-if="'recalibrate' in recalibrationLinks" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
: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="recalibrationLinks.recalibrate.href"
|
||||
:submit-label="'Full Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_exposure_from_raw' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_exposure_from_raw.href"
|
||||
:submit-label="'Auto gain & shutter speed'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_white_balance_from_raw' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href"
|
||||
:submit-label="'Auto white balance'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_lens_shading_table' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<taskSubmitter
|
||||
: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="recalibrationLinks.auto_lens_shading_table.href"
|
||||
:submit-label="'Auto flat field correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="showExtraSettings" class="uk-child-width-expand">
|
||||
<button
|
||||
v-if="'flatten_lens_shading_table' in recalibrationLinks"
|
||||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
@click="flattenLensShadingTableRequest"
|
||||
>
|
||||
Disable flat field correction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="LstDownloadEnabled">
|
||||
<a
|
||||
class="uk-button uk-button-default uk-width-large uk-margin-small-top uk-align-center"
|
||||
:href="LstDownloadUri"
|
||||
download
|
||||
>Download Lens-Shading Table</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../../genericComponents/taskSubmitter";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CameraCalibrationSettings",
|
||||
|
||||
components: {
|
||||
taskSubmitter
|
||||
},
|
||||
|
||||
props: {
|
||||
showExtraSettings: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
recalibrationLinks: {},
|
||||
isCalibrating: false,
|
||||
LstDownloadEnabled: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
LstDownloadUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/camera/lst`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateRecalibrationLinks();
|
||||
this.checkLstDownload();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.calibration.picamera"
|
||||
);
|
||||
// if AutocalibrationPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
checkLstDownload: function() {
|
||||
axios
|
||||
.get(this.LstDownloadUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
this.LstDownloadEnabled = true;
|
||||
} else {
|
||||
this.LstDownloadEnabled = false;
|
||||
}
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<div id="appSettings" class="uk-width-large">
|
||||
<h3>Additional features</h3>
|
||||
<p class="uk-margin-small">
|
||||
<label
|
||||
><input v-model="IHIEnabled" class="uk-checkbox" type="checkbox" />
|
||||
Enable IHI Slide Scan</label
|
||||
>
|
||||
</p>
|
||||
<p class="uk-margin-small">
|
||||
Enabling IHI Slide Scan will add a new tab designed to simplify acquiring
|
||||
blood smear tile scans. <br />
|
||||
While this may be useful for other applications, it is primarily designed
|
||||
for use in clinics acquiring blood smear samples using a 100x
|
||||
oil-immersion objective, with a Raspberry Pi Camera v2.
|
||||
</p>
|
||||
<p class="uk-margin-small" :class="{ 'uk-text-muted': !imjoyPermitted }">
|
||||
<label :disabled="!imjoyPermitted">
|
||||
<input
|
||||
v-model="imjoyEnabled"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
:disabled="!imjoyPermitted"
|
||||
/>
|
||||
Enable ImJoy plugin engine
|
||||
</label>
|
||||
</p>
|
||||
<p v-if="imjoyPermitted" class="uk-margin-small">
|
||||
<a href="https://imjoy.io/">ImJoy</a> enables integration with a wide
|
||||
variety of microscopy and image analysis applications, including ImageJ.JS
|
||||
and Kaibu. Support for ImJoy within the OFM software is currently
|
||||
experimental, and it may slow down loading of the application.
|
||||
</p>
|
||||
<p v-if="!imjoyPermitted" class="uk-margin-small uk-text-muted">
|
||||
ImJoy plugins are disabled in this build of the OpenFlexure software.
|
||||
</p>
|
||||
<p class="uk-margin-small">
|
||||
<label
|
||||
><input v-model="galleryEnabled" class="uk-checkbox" type="checkbox" />
|
||||
Enable Gallery</label
|
||||
>
|
||||
</p>
|
||||
<p class="uk-margin-small">
|
||||
The "gallery" tab can cause performance issues; un-tick the box above to
|
||||
disable it.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "FeaturesSettings",
|
||||
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
IHIEnabled: {
|
||||
get() {
|
||||
return this.$store.state.IHIEnabled;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeIHIEnabled", value);
|
||||
}
|
||||
},
|
||||
imjoyEnabled: {
|
||||
get() {
|
||||
return this.$store.state.imjoyEnabled;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeImjoyEnabled", value);
|
||||
}
|
||||
},
|
||||
galleryEnabled: {
|
||||
get() {
|
||||
return this.$store.state.galleryEnabled;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeGalleryEnabled", value);
|
||||
}
|
||||
},
|
||||
imjoyPermitted: function() {
|
||||
// ImJoy may be disabled using an environment variable.
|
||||
// If this function returns false, it is never possible to
|
||||
// use ImJoy, so we should gray out the option.
|
||||
return process.env.VUE_APP_ENABLE_IMJOY === "true";
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
IHIEnabled: function() {
|
||||
this.setLocalStorageObj("IHIEnabled", this.IHIEnabled);
|
||||
},
|
||||
imjoyEnabled: function() {
|
||||
this.setLocalStorageObj("imjoyEnabled", this.imjoyEnabled);
|
||||
},
|
||||
galleryEnabled: function() {
|
||||
this.setLocalStorageObj("galleryEnabled", this.galleryEnabled);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Try loading settings from localStorage. If null, don't change.
|
||||
this.IHIEnabled = this.getLocalStorageObj("IHIEnabled") || this.IHIEnabled;
|
||||
this.imjoyEnabled =
|
||||
this.getLocalStorageObj("imjoyEnabled") || this.imjoyEnabled;
|
||||
this.galleryEnabled =
|
||||
this.getLocalStorageObj("galleryEnabled") || this.galleryEnabled;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div v-if="settings" id="microscopeSettings" class="uk-width-large">
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Microscope name</label
|
||||
>
|
||||
<input
|
||||
v-model="settings.name"
|
||||
class="uk-input uk-width-1-1 uk-form-small"
|
||||
name="inputFilename"
|
||||
placeholder="Leave blank for default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "MicroscopeSettings",
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
applySettingsRequest: function() {
|
||||
var payload = {
|
||||
name: this.settings.name
|
||||
};
|
||||
|
||||
// Send request to update config
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Microscope settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.center-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<div id="stageSettings" class="uk-width-large">
|
||||
<div v-if="stageType === 'MissingStage'" class="uk-text-danger">
|
||||
<b>No stage connected</b>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3>Stage settings</h3>
|
||||
<form @submit.prevent="setStageType">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<h4>Stage geometry</h4>
|
||||
<select v-model="stageType" class="uk-select uk-margin-small-top">
|
||||
<option value="SangaStage">SangaStage (Standard)</option>
|
||||
<option value="SangaDeltaStage"> SangaStage (Delta)</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Change stage geometry
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<h4>Backlash compensation</h4>
|
||||
<p class="uk-margin-remove">
|
||||
Backlash compentation causes movements to overshoot by that number
|
||||
of motor steps, reducing the effect of mechanical backlash.
|
||||
</p>
|
||||
<div
|
||||
class="uk-grid-small uk-child-width-1-3 uk-margin-small-bottom"
|
||||
uk-grid
|
||||
>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.x"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.y"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Settle time</h4>
|
||||
<div v-if="settle_time !== undefined">
|
||||
<p class="uk-margin-small-bottom">
|
||||
When moving or scanning, captures will be delayed by the settle
|
||||
time (in seconds) to reduce motion blur.
|
||||
</p>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="settle_time"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "StageSettings",
|
||||
|
||||
components: {},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
stageType: "MissingStage",
|
||||
backlash: {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
z: undefined
|
||||
},
|
||||
settle_time: undefined
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
stageTypeUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/stage/type`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getStageType();
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
const stageSettings = response.data.stage;
|
||||
this.backlash.x = stageSettings.backlash.x;
|
||||
this.backlash.y = stageSettings.backlash.y;
|
||||
this.backlash.z = stageSettings.backlash.z;
|
||||
this.settle_time = stageSettings.settle_time;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
applySettingsRequest: function() {
|
||||
var payload = {
|
||||
stage: {
|
||||
backlash: this.backlash
|
||||
}
|
||||
};
|
||||
// Send request to update settings
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Stage settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
getStageType: function() {
|
||||
axios
|
||||
.get(this.stageTypeUri)
|
||||
.then(response => {
|
||||
this.stageType = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error);
|
||||
});
|
||||
},
|
||||
setStageType: function() {
|
||||
axios
|
||||
.put(this.stageTypeUri, this.stageType, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.stageType = response.data;
|
||||
|
||||
this.modalNotify("Stage geometry changed.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<div id="streamSettings" class="uk-width-large">
|
||||
<div>
|
||||
<h3>Stream settings</h3>
|
||||
<label
|
||||
><input v-model="disableStream" class="uk-checkbox" type="checkbox" />
|
||||
Disable web stream</label
|
||||
>
|
||||
<p class="uk-margin-small">
|
||||
This will disable the embedded web stream of the camera.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<h3>Microscope display output</h3>
|
||||
<div uk-grid>
|
||||
<div>
|
||||
<label :class="[{ 'uk-disabled': !this.$store.getters.ready }]"
|
||||
><input
|
||||
v-model="autoGpuPreview"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Enable GPU preview</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label :class="[{ 'uk-disabled': !this.$store.getters.ready }]"
|
||||
><input v-model="trackWindow" class="uk-checkbox" type="checkbox" />
|
||||
Track window</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Enable GPU preview turns on a low-latency camera preview drawn directly
|
||||
to the Raspberry Pi display output.
|
||||
</p>
|
||||
<p class="uk-margin-small">
|
||||
<b
|
||||
>Content, such as your mouse, won't be visible behind this preview.</b
|
||||
>
|
||||
Track Window will attempt to resize the GPU preview based on the current
|
||||
window size, however this is often imperfect and cannot track window
|
||||
movement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "StreamSettings",
|
||||
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
disableStream: {
|
||||
get() {
|
||||
return this.$store.state.disableStream;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeDisableStream", value);
|
||||
}
|
||||
},
|
||||
|
||||
autoGpuPreview: {
|
||||
get() {
|
||||
return this.$store.state.autoGpuPreview;
|
||||
},
|
||||
set(value) {
|
||||
// NB the stream viewer watches the store, and is
|
||||
// responsible for making the request that switches
|
||||
// GPU preview on/off
|
||||
// see streamContent.vue
|
||||
this.$store.commit("changeAutoGpuPreview", value);
|
||||
}
|
||||
},
|
||||
|
||||
trackWindow: {
|
||||
get() {
|
||||
return this.$store.state.trackWindow;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeTrackWindow", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Cache the stream settings to local storage for persistence
|
||||
// (the next 3 functions all relate to this)
|
||||
disableStream: function(newValue) {
|
||||
this.setLocalStorageObj("disableStream", newValue);
|
||||
},
|
||||
autoGpuPreview: function(newValue) {
|
||||
this.setLocalStorageObj("autoGpuPreview", newValue);
|
||||
},
|
||||
trackWindow: function(newValue) {
|
||||
this.setLocalStorageObj("trackWindow", newValue);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
// Apply sensible defaults for stream settings, depending on
|
||||
// whether we're connecting locally or remotely, respecting
|
||||
// the settings that were cached previously.
|
||||
const localMode = ["localhost", "0.0.0.0", "127.0.0.1", "[::1]"].includes(
|
||||
window.location.hostname
|
||||
);
|
||||
const localDefaults = {
|
||||
disableStream: true,
|
||||
autoGpuPreview: true,
|
||||
trackWindow: true
|
||||
};
|
||||
for (let k in localDefaults) {
|
||||
if (localStorage.getItem(k) !== null) {
|
||||
this[k] = this.getLocalStorageObj(k);
|
||||
} else if (localMode) {
|
||||
console.log(`${k} set to default value for a local connection`);
|
||||
this[k] = localDefaults[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
218
webapp/src/components/tabContentComponents/settingsContent.vue
Normal file
218
webapp/src/components/tabContentComponents/settingsContent.vue
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="settings-nav">
|
||||
<ul class="uk-nav uk-nav-default">
|
||||
<li class="uk-nav-header">Application settings</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-display-icon"
|
||||
tab-i-d="display"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Display
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-features-icon"
|
||||
tab-i-d="features"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Features
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li class="uk-nav-header">Microscope settings</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-camera-icon"
|
||||
tab-i-d="camera"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Camera
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-stage-icon"
|
||||
tab-i-d="stage"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Stage
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-mapping-icon"
|
||||
tab-i-d="mapping"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Camera/stage mapping
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-microscope-icon"
|
||||
tab-i-d="microscope"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
General
|
||||
</tabIcon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="view-component uk-width-expand uk-padding-small">
|
||||
<tabContent
|
||||
tab-i-d="display"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<appSettings />
|
||||
<streamSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="features"
|
||||
:require-connection="false"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<featuresSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="camera"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<cameraSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="stage"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<stageSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="mapping"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<CSMSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="microscope"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<h3>Microscope settings</h3>
|
||||
<microscopeSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import streamSettings from "./settingsComponents/streamSettings.vue";
|
||||
import microscopeSettings from "./settingsComponents/microscopeSettings.vue";
|
||||
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
||||
import appSettings from "./settingsComponents/appSettings.vue";
|
||||
import featuresSettings from "./settingsComponents/featuresSettings.vue";
|
||||
import CSMSettings from "./settingsComponents/CSMSettings.vue";
|
||||
import stageSettings from "./settingsComponents/stageSettings.vue";
|
||||
// Import generic components
|
||||
import tabIcon from "../genericComponents/tabIcon";
|
||||
import tabContent from "../genericComponents/tabContent";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "SettingsContent",
|
||||
|
||||
components: {
|
||||
streamSettings,
|
||||
cameraSettings,
|
||||
stageSettings,
|
||||
microscopeSettings,
|
||||
CSMSettings,
|
||||
appSettings,
|
||||
featuresSettings,
|
||||
tabIcon,
|
||||
tabContent
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
selected: "display",
|
||||
currentTab: "display"
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
setTab: function(event, tab) {
|
||||
if (!(this.currentTab == tab)) {
|
||||
this.currentTab = tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// Custom UIkit CSS modifications
|
||||
@import "../../assets/less/theme.less";
|
||||
|
||||
.settings-nav {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
width: 250px;
|
||||
padding: 10px;
|
||||
background-color: rgba(180, 180, 180, 0.03);
|
||||
border-width: 0 1px 0 0;
|
||||
border-style: solid;
|
||||
border-color: rgba(180, 180, 180, 0.25);
|
||||
}
|
||||
|
||||
.settings-nav li > a {
|
||||
padding-left: 6px !important;
|
||||
border-radius: @button-border-radius;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
<template>
|
||||
<div class="uk-padding-small">
|
||||
<div v-show="!scanUri">No scan extension found</div>
|
||||
<div v-show="scanUri">
|
||||
<form @submit.prevent @keyup.enter="increment()">
|
||||
<div v-show="stepValue == 0" id="step-user">
|
||||
<h3>User information (1/4)</h3>
|
||||
|
||||
<label for="username">Your name:</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
class="uk-input"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<label for="currentTime">Current time (check this is correct):</label>
|
||||
<input
|
||||
id="currentTime"
|
||||
v-model="currentTimeForm"
|
||||
class="uk-input"
|
||||
type="datetime-local"
|
||||
name="currentTime"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="stepValue == 1" id="step-patient">
|
||||
<h3>Patient information (2/4)</h3>
|
||||
|
||||
<label for="patientID">Patient ID:</label>
|
||||
<input
|
||||
id="patientID"
|
||||
v-model="patientID"
|
||||
class="uk-input"
|
||||
type="text"
|
||||
name="patientID"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div v-show="stepValue == 2" id="step-sample">
|
||||
<h3>Sample information (3/4)</h3>
|
||||
|
||||
<label>Sample type:</label>
|
||||
<br />
|
||||
<input
|
||||
id="typeThin"
|
||||
v-model="sampleType"
|
||||
class="uk-radio"
|
||||
type="radio"
|
||||
value="thin"
|
||||
/>
|
||||
<label for="typeThin">Thin smear</label>
|
||||
<br />
|
||||
<input
|
||||
id="typeThick"
|
||||
v-model="sampleType"
|
||||
class="uk-radio"
|
||||
type="radio"
|
||||
value="thick"
|
||||
/>
|
||||
<label for="typeThick">Thick smear</label>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div v-show="stepValue == 3" id="step-scan">
|
||||
<h3>Start scan (4/4)</h3>
|
||||
|
||||
<label>Check details:</label>
|
||||
<p>
|
||||
<b>Your name:</b>
|
||||
{{ username }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Patient ID:</b>
|
||||
{{ patientID }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Sample type:</b>
|
||||
{{ sampleType }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="form-stepper">
|
||||
<p class="warning">{{ message }}</p>
|
||||
<taskSubmitter
|
||||
v-if="scanUri"
|
||||
v-show="stepValue == 3"
|
||||
:submit-url="scanUri"
|
||||
:submit-data="payload"
|
||||
submit-label="Start scan"
|
||||
@submit="scanRunning = true"
|
||||
@response="scanRunning = false"
|
||||
@error="onScanError"
|
||||
></taskSubmitter>
|
||||
<br />
|
||||
|
||||
<div v-show="!scanRunning" class="grid-container">
|
||||
<button
|
||||
class="uk-button uk-button-primary"
|
||||
:disabled="stepValue <= 0"
|
||||
type="button"
|
||||
@click="decrement()"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
class="uk-button uk-button-primary"
|
||||
:disabled="stepValue >= 3"
|
||||
type="button"
|
||||
@click="increment()"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
<button
|
||||
v-show="stepValue == 3 && !scanRunning"
|
||||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
:disabled="scanRunning"
|
||||
type="button"
|
||||
@click="restart()"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
taskSubmitter
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
scanUri: null,
|
||||
stepValue: 0,
|
||||
hostDeviceName: null,
|
||||
message: null,
|
||||
// Scan info
|
||||
username: "",
|
||||
currentTime: this.getLocalDatetimeString(),
|
||||
patientID: "",
|
||||
sampleType: "thin",
|
||||
scanRunning: false,
|
||||
// Scan settings
|
||||
bayer: false,
|
||||
grid: [10, 10, 9],
|
||||
stride_size: [800, 600, 10],
|
||||
fast_autofocus: true,
|
||||
autofocus_dz: 2000,
|
||||
style: "raster",
|
||||
use_video_port: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
currentTimeForm: {
|
||||
get() {
|
||||
// Chop the timezone information from the end
|
||||
return this.currentTime.substr(0, 19);
|
||||
},
|
||||
set(val) {
|
||||
// Get timezone
|
||||
var dt = new Date();
|
||||
var tzo = -dt.getTimezoneOffset(),
|
||||
dif = tzo >= 0 ? "+" : "-",
|
||||
pad = function(num) {
|
||||
var norm = Math.floor(Math.abs(num));
|
||||
return (norm < 10 ? "0" : "") + norm;
|
||||
};
|
||||
// Stick to the end of the new timestring from the form
|
||||
this.currentTime = val + dif + pad(tzo / 60) + ":" + pad(tzo % 60);
|
||||
}
|
||||
},
|
||||
|
||||
payload: {
|
||||
get() {
|
||||
return {
|
||||
filename: `${this.patientID}_${this.sampleType}`,
|
||||
temporary: false,
|
||||
bayer: this.bayer,
|
||||
grid: this.grid,
|
||||
stride_size: this.stride_size,
|
||||
fast_autofocus: this.fast_autofocus,
|
||||
autofocus_dz: this.autofocus_dz,
|
||||
use_video_port: this.use_video_port,
|
||||
annotations: {
|
||||
patientID: this.patientID,
|
||||
username: this.username,
|
||||
clientDatetime: this.currentTime
|
||||
},
|
||||
tags: ["slidescan"]
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.updateScanUri();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateScanUri: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.scan"
|
||||
);
|
||||
// if ScanPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.scanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onScanError: function(error) {
|
||||
this.scanRunning = false;
|
||||
this.modalError(error);
|
||||
},
|
||||
|
||||
decrement: function() {
|
||||
if (this.stepValue > 0) {
|
||||
this.stepValue = this.stepValue - 1;
|
||||
}
|
||||
},
|
||||
|
||||
increment: function() {
|
||||
// Validate sections
|
||||
if (this.stepValue == 0) {
|
||||
if (!this.username) {
|
||||
this.message = "Please enter your name";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.stepValue == 1) {
|
||||
if (!this.patientID) {
|
||||
this.message = "Please enter a patient ID";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Upper bound on section number
|
||||
if (this.stepValue < 3) {
|
||||
this.stepValue = this.stepValue + 1;
|
||||
this.message = "";
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
restart: function() {
|
||||
this.stepValue = 0;
|
||||
this.patientID = "";
|
||||
this.currentTime = this.getLocalDatetimeString();
|
||||
},
|
||||
|
||||
getLocalDatetimeString: function() {
|
||||
var dt = new Date();
|
||||
var tzo = -dt.getTimezoneOffset(),
|
||||
dif = tzo >= 0 ? "+" : "-",
|
||||
pad = function(num) {
|
||||
var norm = Math.floor(Math.abs(num));
|
||||
return (norm < 10 ? "0" : "") + norm;
|
||||
};
|
||||
return (
|
||||
dt.getFullYear() +
|
||||
"-" +
|
||||
pad(dt.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(dt.getDate()) +
|
||||
"T" +
|
||||
pad(dt.getHours()) +
|
||||
":" +
|
||||
pad(dt.getMinutes()) +
|
||||
":" +
|
||||
pad(dt.getSeconds()) +
|
||||
dif +
|
||||
pad(tzo / 60) +
|
||||
":" +
|
||||
pad(tzo % 60)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: 10px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #c11;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="control-component">
|
||||
<paneSlideScan />
|
||||
</div>
|
||||
<div class="view-component uk-width-expand">
|
||||
<streamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import paneSlideScan from "./slideScanComponents/paneSlideScan";
|
||||
import streamDisplay from "./streamContent.vue";
|
||||
|
||||
export default {
|
||||
name: "SlideScanContent",
|
||||
|
||||
components: {
|
||||
paneSlideScan,
|
||||
streamDisplay
|
||||
}
|
||||
};
|
||||
</script>
|
||||
317
webapp/src/components/tabContentComponents/streamContent.vue
Normal file
317
webapp/src/components/tabContentComponents/streamContent.vue
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<div
|
||||
id="stream-display"
|
||||
ref="streamDisplay"
|
||||
v-observe-visibility="visibilityChanged"
|
||||
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
||||
>
|
||||
<img
|
||||
v-if="isVisible"
|
||||
ref="click-frame"
|
||||
class="uk-align-center uk-margin-remove-bottom"
|
||||
:hidden="!streamEnabled"
|
||||
:src="streamImgUri"
|
||||
alt="Stream"
|
||||
@dblclick="clickMonitor"
|
||||
/>
|
||||
|
||||
<div v-if="!streamEnabled" class="uk-height-1-1">
|
||||
<div v-if="$store.state.waiting" class="uk-position-center">
|
||||
<div uk-spinner="ratio: 4.5"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="$store.state.disableStream"
|
||||
class="uk-position-center position-relative text-center"
|
||||
>
|
||||
Stream preview disabled
|
||||
</div>
|
||||
|
||||
<div v-else class="uk-position-center position-relative text-center">
|
||||
No active connection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "StreamDisplay",
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
isVisible: false,
|
||||
displaySize: [0, 0],
|
||||
displayPosition: [0, 0],
|
||||
resizeTimeoutId: setTimeout(this.doneResizing, 500)
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
streamEnabled: function() {
|
||||
return this.$store.getters.ready && !this.$store.state.disableStream;
|
||||
},
|
||||
thisStreamOpen: function() {
|
||||
// Only a single MJPEG connection should be open at a time
|
||||
return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0);
|
||||
},
|
||||
streamImgUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/streams/mjpeg`;
|
||||
},
|
||||
startPreviewUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/start`;
|
||||
},
|
||||
stopPreviewUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/stop`;
|
||||
},
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
autoGpuPreview: function() {
|
||||
return this.$store.state.autoGpuPreview;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
autoGpuPreview: function(newValue) {
|
||||
// When the GPU preview setting in the store changes, update the server
|
||||
this.safePreviewRequest(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// A global signal listener to change the GPU preview state
|
||||
this.$root.$on("globalTogglePreview", state => {
|
||||
this.previewRequest(state);
|
||||
});
|
||||
// A global signal listener to flash the stream element
|
||||
this.$root.$on("globalFlashStream", () => {
|
||||
this.flashStream();
|
||||
});
|
||||
|
||||
// Mutation observer
|
||||
this.sizeObserver = new ResizeObserver(() => {
|
||||
this.handleResize(); // For any element attached to the observer, run handleResize() on change
|
||||
});
|
||||
// Fetch streamDisplay component by ref
|
||||
const streamDisplayElement = this.$refs.streamDisplay.parentNode;
|
||||
// Attach streamDisplay to the size observer
|
||||
this.sizeObserver.observe(streamDisplayElement);
|
||||
},
|
||||
|
||||
created: function() {
|
||||
// Send a request to start/stop GPU preview based on global setting
|
||||
this.safePreviewRequest(this.autoGpuPreview);
|
||||
},
|
||||
|
||||
beforeDestroy: function() {
|
||||
// Remove global signal listener to change the GPU preview state
|
||||
this.$root.$off("globalTogglePreview");
|
||||
// Remove global signal listener to flash the stream element
|
||||
this.$root.$off("globalFlashStream");
|
||||
// Disconnect the size observer
|
||||
this.sizeObserver.disconnect();
|
||||
// Remove from the array of active streams
|
||||
this.$store.commit("removeStream", this._uid);
|
||||
},
|
||||
|
||||
methods: {
|
||||
visibilityChanged(isVisible) {
|
||||
this.isVisible = isVisible;
|
||||
},
|
||||
flashStream: function() {
|
||||
// Run an animation that flashes the stream (for capture feedback)
|
||||
let element = this.$refs.streamDisplay;
|
||||
element.classList.remove("uk-animation-fade");
|
||||
element.offsetHeight; /* trigger reflow */
|
||||
element.classList.add("uk-animation-fade");
|
||||
setTimeout(function() {
|
||||
element.classList.remove("uk-animation-fade");
|
||||
}, 800);
|
||||
},
|
||||
|
||||
clickMonitor: function(event) {
|
||||
// Calculate steps from event coordinates
|
||||
let xCoordinate = event.offsetX;
|
||||
let yCoordinate = event.offsetY;
|
||||
|
||||
// Simply scaling by naturalHeight/offsetHeight may give the wrong answer!
|
||||
// because we use content-fit: contain in the stylesheet, the img element
|
||||
// may be larger than the picture. So, we must determine whether the
|
||||
// width or height is setting the scaling factor, and use a uniform scale
|
||||
// factor.
|
||||
let scale = Math.max(
|
||||
event.target.naturalWidth / event.target.offsetWidth,
|
||||
event.target.naturalHeight / event.target.offsetHeight
|
||||
);
|
||||
|
||||
let xRelative = (0.5 * event.target.offsetWidth - xCoordinate) * scale;
|
||||
let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale;
|
||||
|
||||
// Emit a signal to move, acted on by panelNavigate.vue
|
||||
this.$root.$emit(
|
||||
"globalMoveInImageCoordinatesEvent",
|
||||
-xRelative,
|
||||
-yRelative
|
||||
);
|
||||
},
|
||||
|
||||
handleResize: function() {
|
||||
// Only fires resize event after no resize in 500ms (prevents resize event spam)
|
||||
clearTimeout(this.resizeTimeoutId);
|
||||
this.resizeTimeoutId = setTimeout(this.handleDoneResize, 250);
|
||||
},
|
||||
|
||||
handleDoneResize: function() {
|
||||
// Recalculate size
|
||||
|
||||
this.recalculateSize();
|
||||
// Handle closed stream
|
||||
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
||||
// If this stream was previously active
|
||||
if (this.$store.state.activeStreams[this._uid] == true) {
|
||||
this.$store.commit("removeStream", this._uid);
|
||||
// If all streams are closed, request the GPU preview close
|
||||
var a = Object.values(this.$store.state.activeStreams);
|
||||
let allClosed = a.every(v => v === false);
|
||||
if (allClosed) {
|
||||
this.safePreviewRequest(false);
|
||||
}
|
||||
}
|
||||
// If resized to anything other than zero
|
||||
} else {
|
||||
this.$store.commit("addStream", this._uid);
|
||||
if (this.$store.state.autoGpuPreview == true) {
|
||||
// Start the preview immediately
|
||||
this.safePreviewRequest(true);
|
||||
// Send another start preview request after a timeout
|
||||
/*
|
||||
The internal logic of this component means that a stop GPU preview
|
||||
request will only ever be sent if ALL stream components are invisible.
|
||||
However, when switching tabs, sometimes the previous component will
|
||||
react to becoming invisible before the new tab has become visible.
|
||||
This results in a stop-preview request being sent JUST before the new
|
||||
start preview request is sent. In an ideal network, this is fine, however
|
||||
due to network jitter, these requests will occasionally be recieved out
|
||||
of order, causing the preview to start in the new location, and then
|
||||
quickly stop again.
|
||||
Preventing this properly will involve some clever server-side logic
|
||||
probably, however as a pit-stop solution, we always send another
|
||||
start-preview request after 1 second. This means that either:
|
||||
1. The initial requests happened in order, in which case this follow-up
|
||||
request does nothing, or
|
||||
2. The requests leapfrog eachother, stopping the preview, in which case
|
||||
the follow-up request restarts the preview in the correct location.
|
||||
*/
|
||||
setTimeout(() => this.safePreviewRequest(true), 250);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recalculateSize: function() {
|
||||
// Calculate stream size
|
||||
let element = this.$refs.streamDisplay.parentNode;
|
||||
let bound = element.getBoundingClientRect();
|
||||
|
||||
let elementSize = [bound.width, bound.height];
|
||||
|
||||
let elementPositionOnWindow = [bound.left, bound.top];
|
||||
let windowPositionOnDisplay = [window.screenX, window.screenY];
|
||||
let windowChromeHeight = window.outerHeight - window.innerHeight;
|
||||
let elementPositionOnDisplay = [
|
||||
Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
|
||||
Math.max(
|
||||
0,
|
||||
windowPositionOnDisplay[1] +
|
||||
elementPositionOnWindow[1] +
|
||||
windowChromeHeight
|
||||
)
|
||||
];
|
||||
|
||||
this.displaySize = elementSize;
|
||||
this.displayPosition = elementPositionOnDisplay;
|
||||
},
|
||||
|
||||
safePreviewRequest: function(state) {
|
||||
// previewRequest, but only stopping preview if all streams are invisible
|
||||
// and only starting preview if any stream is visible
|
||||
var a = Object.values(this.$store.state.activeStreams);
|
||||
let allClosed = a.every(v => v === false);
|
||||
// If all streams are closed, don't start GPU preview
|
||||
if (state === true && allClosed) {
|
||||
return false;
|
||||
}
|
||||
// If not all streams are closed, don't stop GPU preview
|
||||
if (state === false && !allClosed) {
|
||||
return false;
|
||||
}
|
||||
return this.previewRequest(state);
|
||||
},
|
||||
|
||||
previewRequest: function(state) {
|
||||
// If requesting starting the stream, but this component is inactive, skip
|
||||
if (
|
||||
this.$store.getters.ready == true &&
|
||||
this.$store.state.autoGpuPreview == true
|
||||
) {
|
||||
var requestUri = null;
|
||||
// Create URI
|
||||
if (state == true) {
|
||||
requestUri = this.startPreviewUri;
|
||||
} else {
|
||||
requestUri = this.stopPreviewUri;
|
||||
}
|
||||
|
||||
// Generate payload if tracking window position
|
||||
var payload = {};
|
||||
if (this.$store.state.trackWindow == true && state == true) {
|
||||
// Recalculate frame dimensions and position
|
||||
this.recalculateSize();
|
||||
// Copy data into payload array
|
||||
payload = {
|
||||
window: [
|
||||
this.displayPosition[0],
|
||||
this.displayPosition[1],
|
||||
this.displaySize[0],
|
||||
this.displaySize[1]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Send preview request
|
||||
|
||||
axios
|
||||
.post(requestUri, payload)
|
||||
.then(() => {})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.stream-display img {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stream-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
20
webapp/src/components/tabContentComponents/viewContent.vue
Normal file
20
webapp/src/components/tabContentComponents/viewContent.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<!-- Grid managing tab content -->
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="view-component uk-width-expand">
|
||||
<streamDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import streamDisplay from "./streamContent.vue";
|
||||
|
||||
export default {
|
||||
name: "ViewContent",
|
||||
|
||||
components: {
|
||||
streamDisplay
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue