502 lines
15 KiB
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"
|
|
/> Plugins
|
|
</button>
|
|
<div uk-dropdown>
|
|
<ul class="uk-nav uk-dropdown-nav">
|
|
<li>
|
|
<a href="#" @click="loadPluginDialog()">
|
|
<i class="material-icons">add</i> 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"
|
|
/> 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"
|
|
/> 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> {{ 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>
|