Switched client from submodule to included
This commit is contained in:
commit
98819403b3
68 changed files with 22018 additions and 5 deletions
|
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div
|
||||
class="hostCard uk-card uk-card-default uk-padding-remove uk-width-medium"
|
||||
:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }"
|
||||
>
|
||||
<div class="uk-card-media-top">
|
||||
<div class="snapshot-container uk-width-1-1 uk-height-auto">
|
||||
<img
|
||||
class="uk-width-1-1"
|
||||
:data-src="snapshotSrc"
|
||||
:src="snapshotSrc"
|
||||
width="300"
|
||||
height="225"
|
||||
/>
|
||||
<div
|
||||
v-if="!snapshotAvailable"
|
||||
class="uk-position-cover uk-flex uk-flex-center uk-flex-middle"
|
||||
>
|
||||
Snapshot unavailable
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uk-card-body uk-padding-small">
|
||||
<div
|
||||
class="uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-flex uk-flex-middle"
|
||||
>
|
||||
<div class="uk-width-expand host-description">
|
||||
<div class="host-description">
|
||||
<b>{{ name }}</b>
|
||||
</div>
|
||||
<div class="host-description">{{ hostname }}:{{ port }}</div>
|
||||
</div>
|
||||
<a
|
||||
v-if="deletable"
|
||||
href="#"
|
||||
class="uk-icon uk-width-auto host-delete"
|
||||
@click="$emit('delete')"
|
||||
><i class="material-icons">delete</i></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uk-card-footer uk-padding-small">
|
||||
<button
|
||||
class="uk-button uk-button-default uk-form-small uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
|
||||
@click="$emit('connect')"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "HostCard",
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
hostname: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
port: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
deletable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
snapshotRule: "/api/v2/streams/snapshot",
|
||||
snapshotSrc: "",
|
||||
snapshotAvailable: false,
|
||||
polling: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
routesURL: function() {
|
||||
return `http://${this.hostname}:${this.port}/routes`;
|
||||
},
|
||||
snapshotURL: function() {
|
||||
return `http://${this.hostname}:${this.port}${this.snapshotRule}`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Try to load the microscope snapshot when mounted
|
||||
this.checkSnapshotAvailable();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// Clear the polling timer
|
||||
if (this.polling !== null) {
|
||||
clearInterval(this.polling);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkSnapshotAvailable: function() {
|
||||
// Fetches the list of routes from the host, and starts
|
||||
// a snapshot timer if the snapshot route is available
|
||||
|
||||
// Send tag request
|
||||
axios
|
||||
.get(this.routesURL)
|
||||
.then(response => {
|
||||
// If the host has a snapshot route available
|
||||
if (this.snapshotRule in response.data) {
|
||||
// Tell the view that a snapshot is available
|
||||
this.snapshotAvailable = true;
|
||||
// Update the snapshot URL
|
||||
this.updateSnapshotURL();
|
||||
// If not already polling, start polling
|
||||
if (this.polling === null) {
|
||||
this.updateSnapshotPoll();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Tell the view that a snapshot is not available, and ignore
|
||||
this.snapshotAvailable = false;
|
||||
});
|
||||
},
|
||||
|
||||
updateSnapshotURL: function() {
|
||||
// Set the snapshot image src to the snapshot URL,
|
||||
// adding a timestamp argument to act as a cache-breaker
|
||||
this.snapshotSrc = `${this.snapshotURL}?${Date.now()}`;
|
||||
},
|
||||
|
||||
updateSnapshotPoll: function() {
|
||||
// Start a timer to call updateSnapshotURL periodically
|
||||
this.polling = setInterval(() => {
|
||||
this.updateSnapshotURL();
|
||||
}, 15000);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.host-description {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snapshot-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img[data-src][src*="data:image"] {
|
||||
background: rgba(127, 127, 127, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
<template>
|
||||
<div
|
||||
class="capture-card uk-card uk-card-default uk-padding-remove uk-width-medium"
|
||||
:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }"
|
||||
>
|
||||
<div class="uk-card-media-top">
|
||||
<a class="lightbox-link" :href="imgURL" :data-caption="fileName">
|
||||
<img
|
||||
class="uk-width-1-1"
|
||||
:data-src="thumbURL"
|
||||
:alt="captureState.metadata.image.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">
|
||||
{{ fileName }}
|
||||
</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>{{ captureState.metadata.image.acquisitionDate }}</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">
|
||||
<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.globalSettings.darkMode
|
||||
}"
|
||||
>
|
||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||
<h2 class="uk-modal-title">{{ fileName }}</h2>
|
||||
<p><b>Path: </b>{{ captureState.path }}</p>
|
||||
<p><b>Time: </b>{{ captureState.metadata.image.acquisitionDate }}</p>
|
||||
<p><b>ID: </b>{{ captureState.metadata.image.id }}</p>
|
||||
<p><b>Format: </b>{{ captureState.metadata.image.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.globalSettings.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 keyvalList from "../../fieldComponents/keyvalList";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CaptureCard",
|
||||
|
||||
components: {
|
||||
keyvalList
|
||||
},
|
||||
|
||||
props: {
|
||||
captureState: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
tags: [],
|
||||
newTag: "",
|
||||
annotations: {},
|
||||
newAnnotations: {}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
fileName: function() {
|
||||
return this.captureState.name // If this.captureState.filename exists
|
||||
? this.captureState.name // Use this.captureState.filename
|
||||
: this.captureState.metadata.filename; // Otherwise use old this.captureState.metadata.filename
|
||||
},
|
||||
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.captureState.links.download.href}?thumbnail=true`;
|
||||
},
|
||||
imgURL: function() {
|
||||
return this.captureState.links.download.href;
|
||||
},
|
||||
tagsURL: function() {
|
||||
return this.captureState.links.tags.href;
|
||||
},
|
||||
annotationsURL: function() {
|
||||
return this.captureState.links.annotations.href;
|
||||
},
|
||||
captureURL: function() {
|
||||
return this.captureState.links.self.href;
|
||||
}
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.getTagRequest();
|
||||
this.getAnnotationsRequest();
|
||||
},
|
||||
|
||||
methods: {
|
||||
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) {
|
||||
console.log(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 => {
|
||||
this.tags = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
getAnnotationsRequest: function() {
|
||||
// Send tag request
|
||||
axios
|
||||
.get(this.annotationsURL)
|
||||
.then(response => {
|
||||
this.annotations = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
makeModalName: function(prefix) {
|
||||
return prefix + this.captureState.metadata.image.id;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<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="metadata.image.id"
|
||||
width="300"
|
||||
height="225"
|
||||
uk-img
|
||||
@click="$root.$emit('globalUpdateCaptureFolder', metadata.image.id)"
|
||||
/>
|
||||
</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>{{ metadata.type || "Dataset" }}: </b> {{ metadata.image.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>{{ metadata.image.acquisitionDate }}</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">
|
||||
<span
|
||||
v-for="tag in metadata.image.tags"
|
||||
:key="tag"
|
||||
class="uk-label uk-margin-small-right deletable-label"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div :id="metadataModalID" uk-modal>
|
||||
<div class="uk-modal-dialog uk-modal-body">
|
||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||
<h2 class="uk-modal-title">{{ metadata.image.name }}</h2>
|
||||
<p><b>Time: </b>{{ metadata.image.acquisitionDate }}</p>
|
||||
<p><b>ID: </b>{{ metadata.image.id }}</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<div v-for="(value, key) in metadata.image.annotations" :key="key">
|
||||
<p>
|
||||
<b>{{ key }}: </b>{{ value }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "ScanCard",
|
||||
|
||||
props: {
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
thumbnail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
captures: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
allURLs: function() {
|
||||
var urls = [];
|
||||
for (var capture of this.captures) {
|
||||
urls.push(capture.links.self.href);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
makeModalName: function(prefix) {
|
||||
return prefix + this.metadata.image.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
|
||||
//axios.spread(function(...res) {
|
||||
// all requests are now complete
|
||||
//console.log(res);
|
||||
//})
|
||||
()
|
||||
.then(() => {
|
||||
// Emit signal to update capture list
|
||||
this.$root.$emit("globalUpdateCaptures");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
<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-form-small 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"
|
||||
@submit="onSubmit"
|
||||
@response="onResponse"
|
||||
@error="onError"
|
||||
>
|
||||
</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)
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onResponse: function(response) {
|
||||
this.lastSessionId = response.return.id;
|
||||
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
|
||||
this.downloadReady = true;
|
||||
},
|
||||
|
||||
onSubmit: function(submitData) {
|
||||
console.log("SUBMITTED");
|
||||
console.log(submitData);
|
||||
},
|
||||
|
||||
onError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
<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-navbar-dropdown"
|
||||
:class="{
|
||||
'uk-light uk-background-secondary':
|
||||
$store.state.globalSettings.darkMode
|
||||
}"
|
||||
>
|
||||
<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"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
:value="tag"
|
||||
checked
|
||||
/>
|
||||
{{ tag }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Right side buttons -->
|
||||
<div class="uk-navbar-right">
|
||||
<div class="uk-grid">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="uk-button uk-button-default uk-width-1-1"
|
||||
@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
|
||||
href="#"
|
||||
class="uk-icon uk-margin-remove"
|
||||
@click="galleryFolder = ''"
|
||||
>
|
||||
<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].metadata.image.name }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery capture cards -->
|
||||
<div class="gallery-grid">
|
||||
<div v-for="item in sortedItems" :key="item.metadata.id">
|
||||
<scanCard
|
||||
v-if="'isScan' in item"
|
||||
:metadata="item.metadata"
|
||||
:thumbnail="item.thumbnail"
|
||||
:captures="item.captures"
|
||||
/>
|
||||
<captureCard v-else :capture-state="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
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
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
captures: [],
|
||||
checkedTags: [],
|
||||
sortDescending: true,
|
||||
galleryFolder: "",
|
||||
scanTag: "scan",
|
||||
unwatchStoreFunction: null
|
||||
};
|
||||
},
|
||||
|
||||
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.metadata.image.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 (!capture.metadata.dataset) {
|
||||
captures.push(capture);
|
||||
}
|
||||
}
|
||||
|
||||
return captures;
|
||||
},
|
||||
|
||||
allScans: function() {
|
||||
// List of scans as capture-like objects
|
||||
var scans = {};
|
||||
|
||||
for (var capture of this.captures) {
|
||||
var annotations = capture.metadata.image.annotations;
|
||||
var tags = capture.metadata.image.tags;
|
||||
var dataset = capture.metadata.dataset;
|
||||
|
||||
if (dataset) {
|
||||
var id = dataset["id"];
|
||||
|
||||
// If this scan ID hasn't been seen before
|
||||
if (!(id in scans)) {
|
||||
scans[id] = {};
|
||||
scans[id].isScan = true;
|
||||
scans[id].captures = [];
|
||||
scans[id].metadata = {
|
||||
image: {
|
||||
name: dataset.name,
|
||||
acquisitionDate: dataset.acquisitionDate,
|
||||
id: dataset.id,
|
||||
tags: [],
|
||||
annotations: {}
|
||||
},
|
||||
type: dataset.type
|
||||
};
|
||||
}
|
||||
|
||||
// Add the capture object to the scan
|
||||
scans[id].captures.push(capture);
|
||||
|
||||
// Add missing scan metadata, prioritising first capture
|
||||
for (var key of Object.keys(annotations)) {
|
||||
if (!(key in scans[id].metadata.image.annotations)) {
|
||||
scans[id].metadata.image.annotations[key] = annotations[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Append missing tags
|
||||
for (var tag of tags) {
|
||||
if (!scans[id].metadata.image.tags.includes(tag)) {
|
||||
scans[id].metadata.image.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) {
|
||||
console.log(this.allScans[this.galleryFolder].captures);
|
||||
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.metadata.image.id] = capture;
|
||||
}
|
||||
} else {
|
||||
// If it's a single capture, get the ID of the capture
|
||||
captures[item.metadata.image.id] = item;
|
||||
}
|
||||
}
|
||||
|
||||
return captures;
|
||||
},
|
||||
|
||||
sortedItems: function() {
|
||||
// Sort filteredItems using sortCaptures function
|
||||
return this.sortCaptures(this.filteredItems);
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
// A global signal listener to set the gallery folder
|
||||
this.$root.$on("globalUpdateCaptureFolder", folder => {
|
||||
this.galleryFolder = folder;
|
||||
});
|
||||
},
|
||||
|
||||
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");
|
||||
// Remove global signal listener to set the gallery folder
|
||||
this.$root.$off("globalUpdateCaptureFolder");
|
||||
// Then we call that function here to unwatch
|
||||
if (this.unwatchStoreFunction) {
|
||||
this.unwatchStoreFunction();
|
||||
this.unwatchStoreFunction = null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCaptures: function() {
|
||||
if (this.$store.state.available) {
|
||||
console.log("Updating capture list...");
|
||||
axios
|
||||
.get(this.capturesUri)
|
||||
.then(response => {
|
||||
this.captures = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
} else {
|
||||
console.log("Delaying capture update until connection is available");
|
||||
}
|
||||
},
|
||||
|
||||
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.metadata.image.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.metadata.image.acquisitionDate < b.metadata.image.acquisitionDate)
|
||||
return -1;
|
||||
if (a.metadata.image.acquisitionDate > b.metadata.image.acquisitionDate)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.sortDescending == true) {
|
||||
return list.sort(compare).reverse();
|
||||
} else {
|
||||
return list.sort(compare);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.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;
|
||||
}
|
||||
.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,52 @@
|
|||
<template>
|
||||
<div
|
||||
id="stream-display"
|
||||
ref="streamDisplay"
|
||||
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
||||
>
|
||||
<img
|
||||
ref="click-frame"
|
||||
class="uk-align-center uk-margin-remove-bottom"
|
||||
:src="streamImgUri"
|
||||
alt="Stream"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "MiniStreamDisplay",
|
||||
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
streamImgUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/streams/mjpeg`;
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.stream-display img {
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stream-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div id="appSettings">
|
||||
<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.globalSettings.appTheme;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ["appTheme", value]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
appTheme() {
|
||||
console.log("Saving appTheme setting");
|
||||
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,118 @@
|
|||
<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="'Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="onRecalibrateError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="showExtraSettings" class="uk-child-width-expand" uk-grid>
|
||||
<div v-if="'flatten_lens_shading_table' in recalibrationLinks">
|
||||
<button
|
||||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
@click="flattenLensShadingTableRequest"
|
||||
>
|
||||
Disable flat-field correction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="'delete_lens_shading_table' in recalibrationLinks">
|
||||
<button
|
||||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
@click="deleteLensShadingTableRequest"
|
||||
>
|
||||
Adaptive flat-field correction
|
||||
</button>
|
||||
</div>
|
||||
</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
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateRecalibrationLinks();
|
||||
},
|
||||
|
||||
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
|
||||
});
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished recalibration.");
|
||||
},
|
||||
|
||||
onRecalibrateError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
},
|
||||
|
||||
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,136 @@
|
|||
<template>
|
||||
<div v-if="settings" id="cameraSettings">
|
||||
<div>
|
||||
<h3>Manual camera settings</h3>
|
||||
<form @submit.prevent="applyConfigRequest">
|
||||
<div v-if="settings.picamera">
|
||||
<!--PiCamera settings block-->
|
||||
<div v-if="settings.picamera.shutter_speed !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Exposure time</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="settings.picamera.shutter_speed"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="settings.picamera.analog_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Analogue gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="settings.picamera.analog_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="settings.picamera.digital_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Digital gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="settings.picamera.digital_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<h3>Automatic calibration</h3>
|
||||
<cameraCalibrationSettings></cameraCalibrationSettings>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import cameraCalibrationSettings from "./cameraCalibrationSettings.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CameraSettings",
|
||||
|
||||
components: {
|
||||
cameraCalibrationSettings
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: {}
|
||||
};
|
||||
},
|
||||
|
||||
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.camera;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
applyConfigRequest: function() {
|
||||
console.log("Applying config to the microscope");
|
||||
var payload = {
|
||||
camera: {
|
||||
picamera: {
|
||||
shutter_speed: this.settings.picamera.shutter_speed,
|
||||
analog_gain: this.settings.picamera.analog_gain,
|
||||
digital_gain: this.settings.picamera.digital_gain
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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"></style>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div id="cameraStageMappingSettings">
|
||||
<form @submit.prevent="applyConfigRequest">
|
||||
<!--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="onRecalibrateError"
|
||||
>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CameraStageMappingSettings",
|
||||
|
||||
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 => {
|
||||
console.log("CSM data:");
|
||||
console.log(response.data);
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<div v-if="settings" id="microscopeSettings">
|
||||
<form @submit.prevent="applyConfigRequest">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Backlash compensation</label
|
||||
>
|
||||
<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="settings.stage.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="settings.stage.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="settings.stage.backlash.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<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-form-small uk-float-right 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
|
||||
});
|
||||
},
|
||||
|
||||
applyConfigRequest: function() {
|
||||
var payload = {
|
||||
name: this.settings.name,
|
||||
stage: {
|
||||
backlash: this.settings.stage.backlash
|
||||
}
|
||||
};
|
||||
|
||||
console.log(payload);
|
||||
|
||||
// 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,78 @@
|
|||
<template>
|
||||
<div id="streamSettings">
|
||||
<div>
|
||||
<h3>Stream settings</h3>
|
||||
<label
|
||||
><input v-model="disableStream" class="uk-checkbox" type="checkbox" />
|
||||
Disable live stream</label
|
||||
>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Export main app
|
||||
export default {
|
||||
name: "StreamSettings",
|
||||
|
||||
data: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
disableStream: {
|
||||
get() {
|
||||
return this.$store.state.globalSettings.disableStream;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ["disableStream", value]);
|
||||
}
|
||||
},
|
||||
|
||||
autoGpuPreview: {
|
||||
get() {
|
||||
return this.$store.state.globalSettings.autoGpuPreview;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ["autoGpuPreview", value]);
|
||||
this.$root.$emit("globalSafeTogglePreview", value);
|
||||
}
|
||||
},
|
||||
|
||||
trackWindow: {
|
||||
get() {
|
||||
return this.$store.state.globalSettings.trackWindow;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeSetting", ["trackWindow", value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<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 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-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="camera"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<cameraSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="mapping"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<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>
|
||||
<cameraStageMappingSettings />
|
||||
</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 cameraStageMappingSettings from "./settingsComponents/cameraStageMappingSettings.vue";
|
||||
|
||||
// Import generic components
|
||||
import tabIcon from "../genericComponents/tabIcon";
|
||||
import tabContent from "../genericComponents/tabContent";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "SettingsDisplay",
|
||||
|
||||
components: {
|
||||
streamSettings,
|
||||
cameraSettings,
|
||||
microscopeSettings,
|
||||
cameraStageMappingSettings,
|
||||
appSettings,
|
||||
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">
|
||||
.settings-pane {
|
||||
border-width: 0 1px 0 0;
|
||||
border-style: solid;
|
||||
border-color: rgba(180, 180, 180, 0.25);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<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>
|
||||
<div>
|
||||
<b>Client version:</b> <br />
|
||||
{{ clientVersionName }}
|
||||
</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: {},
|
||||
clientVersion: process.env.PACKAGE.version
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
configurationUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/configuration`;
|
||||
},
|
||||
actionsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions`;
|
||||
},
|
||||
clientVersionName: function() {
|
||||
return `${process.env.PACKAGE.version}`;
|
||||
}
|
||||
},
|
||||
|
||||
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.actionsUri)
|
||||
.then(response => {
|
||||
if ("reboot" in response.data) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"reboot",
|
||||
response.data.reboot.links.self.href
|
||||
);
|
||||
} else {
|
||||
delete this.systemActionLinks.reboot;
|
||||
}
|
||||
if ("shutdown" in response.data) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"shutdown",
|
||||
response.data.shutdown.links.self.href
|
||||
);
|
||||
} 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");
|
||||
axios.post(this.systemActionLinks.shutdown).catch(error => {
|
||||
console.log(error); // Be quiet when empty response is recieved
|
||||
});
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
rebootRequest: function() {
|
||||
this.modalConfirm("Restart microscope?").then(
|
||||
() => {
|
||||
if ("reboot" in this.systemActionLinks) {
|
||||
this.$store.commit("resetState");
|
||||
axios.post(this.systemActionLinks.reboot).catch(error => {
|
||||
console.log(error); // Be quiet when empty response is recieved
|
||||
});
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="less"></style>
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
<template>
|
||||
<div
|
||||
id="stream-display"
|
||||
ref="streamDisplay"
|
||||
class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
|
||||
>
|
||||
<img
|
||||
v-if="thisStreamOpen"
|
||||
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.globalSettings.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 {
|
||||
displaySize: [0, 0],
|
||||
displayPosition: [0, 0],
|
||||
fov: [0, 0],
|
||||
resizeTimeoutId: setTimeout(this.doneResizing, 500)
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
streamEnabled: function() {
|
||||
return (
|
||||
this.$store.getters.ready &&
|
||||
!this.$store.state.globalSettings.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`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log(`${this._uid} mounted`);
|
||||
// A global signal listener to change the GPU preview state
|
||||
this.$root.$on("globalTogglePreview", state => {
|
||||
this.previewRequest(state);
|
||||
});
|
||||
this.$root.$on("globalSafeTogglePreview", state => {
|
||||
this.safePreviewRequest(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() {
|
||||
console.log(`${this._uid} created`);
|
||||
// Send a request to start/stop GPU preview based on global setting
|
||||
this.safePreviewRequest(this.$store.state.globalSettings.autoGpuPreview);
|
||||
// Get FOV from settings
|
||||
this.updateFov();
|
||||
},
|
||||
|
||||
beforeDestroy: function() {
|
||||
console.log(`${this._uid} being destroyed`);
|
||||
// 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: {
|
||||
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");
|
||||
},
|
||||
|
||||
clickMonitor: function(event) {
|
||||
// Calculate steps from event coordinates and store config FOV
|
||||
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
|
||||
console.log(`Recalculating frame size for ${this._uid}`);
|
||||
this.recalculateSize();
|
||||
// Handle closed stream
|
||||
if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
|
||||
console.log(`${this._uid} is zero`);
|
||||
// If this stream was previously active
|
||||
if (this.$store.state.activeStreams[this._uid] == true) {
|
||||
console.log(`${this._uid} was active`);
|
||||
console.log(`STREAM ${this._uid} CLOSED`);
|
||||
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 {
|
||||
console.log(`STREAM ${this._uid} OPEN`);
|
||||
this.$store.commit("addStream", this._uid);
|
||||
if (this.$store.state.globalSettings.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) {
|
||||
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.globalSettings.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
|
||||
console.log(`${this._uid} toggled preview to ${state}`);
|
||||
axios
|
||||
.post(requestUri, payload)
|
||||
.then(() => {})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateFov: function() {
|
||||
console.log("Updating FOV");
|
||||
// Get the current field-of-view setting from the server
|
||||
axios
|
||||
.get(`${this.settingsUri}/fov`)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
this.fov = response.data;
|
||||
}
|
||||
})
|
||||
.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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue