openflexure-microscope-server/openflexure_microscope/api/static/src/components/tabContentComponents/imjoyContent.vue

502 lines
15 KiB
Vue

<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>
<button
v-if="activeWindow"
class="uk-button uk-button-default close-button"
type="button"
@click="closeActiveWindow()"
>
<i class="material-icons">close</i>
</button>
<button class="uk-button uk-button-default plugins-button" type="button">
<img
style="width:18px;"
src="https://imjoy.io/static/img/imjoy-icon.svg"
/>&nbsp;Plugins
</button>
<div uk-dropdown>
<ul class="uk-nav uk-dropdown-nav">
<li>
<a href="#" @click="loadPluginDialog()">
<i class="material-icons">add</i>&nbsp; Load Plugin</a
>
</li>
<li
v-if="Object.keys(loadedPlugins).length > 0"
class="uk-nav-divider"
></li>
<li>
<a href="#" @click="startImageJ()"
><img
alt="ImageJ.JS icon"
style="width:20px;"
src="https://ij.imjoy.io/assets/icons/chrome/chrome-installprocess-128-128.png"
/>&nbsp;ImageJ.JS</a
>
</li>
<li>
<a href="#" @click="startKaibu()"
><img
alt="Kaibu icon"
style="width:20px;"
src="https://kaibu.org/static/img/kaibu-icon.svg"
/>&nbsp;Kaibu</a
>
</li>
<li>
<a
v-for="(p, name) in loadedPlugins"
:key="p.id"
href="#"
@click="runPlugin(p)"
><i class="material-icons">extension</i>{{ name }}</a
>
</li>
<li class="uk-nav-divider"></li>
<li>
<a v-for="w in windows" :key="w.id" href="#" @click="activeWindow = w"
><i class="material-icons">filter_none</i>&nbsp;{{ w.name }}</a
>
</li>
</ul>
</div>
<div
v-for="w in windows"
v-show="w === activeWindow"
:key="w.id"
style="height: 100%;"
>
<h4 class="window-title">{{ w.name }}</h4>
<div :id="w.window_id" class="window-container"></div>
</div>
<div
v-if="!activeWindow || windows.length <= 0"
class="uk-position-center position-relative text-center"
>
<img
style="width:80px;"
src="https://imjoy.io/static/img/imjoy-icon.svg"
/>
</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";
export default {
name: "ImJoyContent",
components: {},
data() {
return {
windows: [],
loading: false,
loadedPlugins: {},
activeWindow: null,
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`;
}
},
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(() => {
console.log("ImJoy started");
imjoy.event_bus.on("add_window", w => {
this.addWindow(w);
});
this.setupMicroscopeAPI();
this.setupViewers();
// load the script editor for openflexure
this.loadPlugin(
"https://gist.githubusercontent.com/oeway/7a9dcb49c51b1f99b2c3619f470fe35c/raw/OpenFlexureScriptEditor.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);
});
}
});
},
setupMicroscopeAPI() {
// Here we register a set of API for controlling the microscope as an 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 getPositionUri = this.getPositionUri;
const moveActionUri = this.moveActionUri;
const modalError = this.modalError;
async function pollUntilComplete(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;
}
const service = {
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 pollUntilComplete(
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 service.setXYZPosition({
"z": z,
"absolute": true,
});
},
async getPosition() {
const pos = await service.getXYZPosition();
return pos.z;
},
async setXYPosition(x, y) {
return service.setXYZPosition({
"x": x,
"y": y,
"absolute": true,
});
},
async getXYPosition() {
const pos = await service.getXYZPosition();
return [pos.x, pos.y];
},
async getXYZPosition() {
return axios
.get(getPositionUri)
.then(r => r.data)
},
async setXYZPosition(payload) {
return axios
.post(moveActionUri, payload)
.then(pollUntilComplete)
}
};
this.imjoy.pm.registerService({ name: "openflexure" }, service);
},
closeActiveWindow() {
if (this.activeWindow) {
this.activeWindow.api.close();
const index = this.windows.indexOf(this.activeWindow);
if (index > -1) {
this.windows.splice(index, 1);
}
if (this.windows.length > 0) {
this.activeWindow = this.windows[this.windows.length - 1];
} else {
this.activeWindow = null;
}
}
},
selectWindow(name) {
for (let w of this.windows) {
if (w.name === name) {
this.activeWindow = w;
break;
}
}
},
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.viewers.kaibu.set_mode("full");
this.selectWindow("Kaibu");
},
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;
});
// load the ITK/VTK viewer for imagej.js
this.loadPlugin(
"https://gist.githubusercontent.com/oeway/e5c980fbf6582f25fde795262a7e33ec/raw/itk-vtk-viewer-imagej.imjoy.html"
);
this.selectWindow("ImageJ.JS");
},
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("addOpenInImjoyMenuItem", {
title: "Kaibu",
async callback(name, imageUrl) {
self.$root.$emit("globalSwitchTab", "ImJoy");
if (!self.viewers.kaibu) {
await self.startKaibu();
}
self.selectWindow("Kaibu");
const viewer = self.viewers.kaibu;
await viewer.view_image(imageUrl, { type: "2d-image", name });
await viewer.add_shapes([]);
}
});
this.$store.commit("addOpenInImjoyMenuItem", {
title: "ImageJ.JS",
async callback(name, imageUrl) {
self.$root.$emit("globalSwitchTab", "ImJoy");
if (!self.viewers.imagej) {
await self.startImageJ();
}
self.selectWindow("ImageJ.JS");
const viewer = self.viewers.imagej;
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
await viewer.viewImage(buffer, {
name: name.replace(".jpeg", ".jpg")
});
}
});
} catch (e) {
alert(`Failed to setup viewers, error: ${e}`);
} finally {
this.loading = false;
}
},
addWindow(w) {
this.windows = this.windows || [];
this.windows.push(w);
this.activeWindow = w;
this.$forceUpdate();
},
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);
}
}
}
};
</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>