Skip to content

Commit

Permalink
feat: ✨ Config Loading & Exporting (#4) (#45)
Browse files Browse the repository at this point in the history
📦 Different exporting types: WebCT and GVXR
- Toggle different sections for WebCT export
- Show Options for different exporting methods
- Easily create master config state object
- Framework for custom data loading and exporting
- Loaders are able to only partially populate the config
- Disable unusable elements for specific exporting types
- Download config as a file with download button

☢ GVXR Exporter:
- Full gvxr json representation, inc typed classes
- Create a gvxr-compatible config object from master config

☢ GVXR Importer:
- Added support for importing from GVXR
    - Parse gvxr config file
        - Load beam properties
            - Load tube type
                - Supports filters
            - Load specific energy level as a synchrotron source type
        - Load detector properties (inc lsf)
        - Load sample properties

🛠 Technical changes:
- Move setting and getting paramaters to functions
- Change SampleProperties to include nullable materialID and material
- Define SamplePropertiesID and SamplePropertiesMat to type specific material format
- Add Settings box in config panel
- Fix buttongroup by removing prefix icon 😢
- Define WebCTConfig type to manage saving/loading all properties
- Spans in checkboxes and switches now include smaller & grey font
- Sample update changes:
    - API now explicitly requires ID materials
    - Match material contents to existing ID materials
    - Create a new material if an existing material contents is not found
    - Automatically use generated new material IDs

🐛 Known Bugs:
- Race condition when importing from GVXR when creating new materials (#47)
    - Current workaround is to refresh and re-import the same config.
  • Loading branch information
WYVERN2742 authored Oct 6, 2022
1 parent 5223e2f commit 094fb38
Show file tree
Hide file tree
Showing 16 changed files with 1,256 additions and 232 deletions.
2 changes: 1 addition & 1 deletion webct/blueprints/app/static/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ function InitialUpdate(): void {
});
}

function UpdatePage(): Promise<void> {
export function UpdatePage(): Promise<void> {
setPageLoading(true);
MarkLoading();
console.log("UpdatePage");
Expand Down
333 changes: 323 additions & 10 deletions webct/blueprints/app/static/js/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,94 @@
import { SlButton, SlDialog, SlIconButton } from "@shoelace-style/shoelace";

// import hljs from "highlight.js"
import { SlButton, SlCheckbox, SlDialog, SlIconButton, SlMenuItem } from "@shoelace-style/shoelace";
import { AlertType, showAlert } from "../../../base/static/js/base";
import { FormatLoader } from "./formats/FormatLoader";
import { GVXRConfig } from "./formats/GVXRLoader";
import { configFull, configSubset, ExportModes as ExportMode, ExportOptions, getConfigKeys, WebCTConfig } from "./types";

let ConfigButton:SlIconButton;
let CloseDialogButton:SlButton;
let ConfigDialog:SlDialog;
let JsonSettingsPanel:HTMLDivElement;
let DownloadConfigButton:SlButton;
let UploadConfigButton:SlButton;

let ModeButton: SlButton;
let ModeJson: SlMenuItem;
let ModeGVXR: SlMenuItem;
let ModeXTEK: SlMenuItem;

let BeamCheckbox: SlCheckbox;
let DetectorCheckbox: SlCheckbox;
let SampleCheckbox: SlCheckbox;
let CaptureCheckbox: SlCheckbox;
let ReconCheckbox: SlCheckbox;

let OptionMatasIDCheckbox: SlCheckbox;

let CodePreview:HTMLPreElement;

// Used for exporting to files
let ConfigContent:string;
let ConfigFormat:ExportMode;

export function setupConfig():boolean {
const button_config = document.getElementById("buttonConfig");
const button_config_close = document.getElementById("buttonConfigClose");
const dialog_config = document.getElementById("dialogueConfig");
const preview_code = document.getElementById("codePreview");
const settings_panel_json = document.getElementById("settingsPanelJson");
const button_config_download = document.getElementById("buttonConfigDownload");
const button_config_upload = document.getElementById("buttonConfigUpload");

const button_mode = document.getElementById("buttonMode");
const menu_mode_json = document.getElementById("menuModeJson");
const menu_mode_gvxr = document.getElementById("menuModeGVXR");
const menu_mode_xtek = document.getElementById("menuModeXTEK");

const checkbox_config_beam = document.getElementById("checkboxConfigBeam");
const checkbox_config_detector = document.getElementById("checkboxConfigDetector");
const checkbox_config_samples = document.getElementById("checkboxConfigSamples");
const checkbox_config_capture = document.getElementById("checkboxConfigCapture");
const checkbox_config_recon = document.getElementById("checkboxConfigRecon");

const checkbox_option_matasid = document.getElementById("checkboxOptMatasID");

if (button_config === null ||
dialog_config == null ||
button_config_close == null ||
preview_code == null) {
preview_code == null ||
button_mode == null ||
settings_panel_json == null ||
button_config_download == null ||
button_config_upload == null ||
menu_mode_json == null ||
menu_mode_gvxr == null ||
menu_mode_xtek == null ||
checkbox_config_beam == null ||
checkbox_config_detector == null ||
checkbox_config_samples == null ||
checkbox_config_capture == null ||
checkbox_config_recon == null ||
checkbox_option_matasid == null) {
console.log(button_config);
console.log(dialog_config);
console.log(button_config_close);
console.log(preview_code);
console.log(settings_panel_json);
console.log(button_config_download);
console.log(button_config_upload);

console.log(button_mode);
console.log(menu_mode_json);
console.log(menu_mode_gvxr);
console.log(menu_mode_xtek);

console.log(checkbox_config_beam);
console.log(checkbox_config_detector);
console.log(checkbox_config_samples);
console.log(checkbox_config_capture);
console.log(checkbox_config_recon);

console.log(checkbox_option_matasid);
return false;
}

Expand All @@ -31,16 +98,262 @@ export function setupConfig():boolean {
ConfigDialog.hide();
};

JsonSettingsPanel = settings_panel_json as HTMLDivElement;
DownloadConfigButton = button_config_download as SlButton;
DownloadConfigButton.onclick = downloadConfig;
UploadConfigButton = button_config_upload as SlButton;
UploadConfigButton.onclick = showUploadConfigDialog;

ModeButton = button_mode as SlButton;
ModeJson = menu_mode_json as SlMenuItem;
ModeGVXR = menu_mode_gvxr as SlMenuItem;
ModeXTEK = menu_mode_xtek as SlMenuItem;

ModeButton.onclick = updateConfigPreview;
ModeJson.onclick = () => {setMode(ExportMode.JSON);};
ModeGVXR.onclick = () => {setMode(ExportMode.GVXR);};
ModeXTEK.onclick = () => {setMode(ExportMode.XTEK);};

BeamCheckbox = checkbox_config_beam as SlCheckbox;
DetectorCheckbox = checkbox_config_detector as SlCheckbox;
SampleCheckbox = checkbox_config_samples as SlCheckbox;
CaptureCheckbox = checkbox_config_capture as SlCheckbox;
ReconCheckbox = checkbox_config_recon as SlCheckbox;

BeamCheckbox.addEventListener("sl-change",updateConfigPreview);
DetectorCheckbox.addEventListener("sl-change", updateConfigPreview);
SampleCheckbox.addEventListener("sl-change", updateConfigPreview);
CaptureCheckbox.addEventListener("sl-change", updateConfigPreview);
ReconCheckbox.addEventListener("sl-change", updateConfigPreview);

OptionMatasIDCheckbox = checkbox_option_matasid as SlCheckbox;
OptionMatasIDCheckbox.addEventListener("sl-change", updateConfigPreview);

CodePreview = preview_code as HTMLPreElement;
ConfigButton = button_config as SlIconButton;
ConfigButton.onclick = () => {
updateConfigPreview();
ConfigDialog.show();
};

CodePreview = preview_code as HTMLPreElement;
CodePreview.textContent = JSON.stringify({
hello: "How are you today"
},undefined,4);

// hljs.highlightElement(CodePreview)

// Set default mode
ConfigFormat = ExportMode.JSON;
return true;
}

function setMode(mode:ExportMode) {
if (mode == ExportMode.XTEK) {
return;
}

ModeGVXR.checked = false;
ModeJson.checked = false;
ModeXTEK.checked = false;

switch (mode) {
case ExportMode.JSON:
ModeButton.textContent = "Format: JSON";
ModeJson.checked = true;
ConfigFormat = ExportMode.JSON;
break;
case ExportMode.GVXR:
ModeButton.textContent = "Format: GVXR";
ModeGVXR.checked = true;
ConfigFormat = ExportMode.GVXR;
break;
// case ExportMode.XTEK:
// ModeButton.textContent = "Format: XTEK";
// ModeXTEK.checked = true;
// ConfigFormat = ExportMode.XTEK;
// break;
}

// Config preview will update the config to the new mode.
updateConfigPreview();
}

function updateConfigPreview() {
// Disable everything as needed
BeamCheckbox.disabled = false;
DetectorCheckbox.disabled = false;
SampleCheckbox.disabled = false;
CaptureCheckbox.disabled = false;
ReconCheckbox.disabled = false;
JsonSettingsPanel.classList.add("hidden");

// Call independent mode update functions
let config:FormatLoader|configSubset;
switch (ConfigFormat) {
case ExportMode.JSON:
config = updateJsonConfig();
break;
case ExportMode.GVXR:
config = updateGvxrConfig();
break;
case ExportMode.XTEK:
config = updateXtekConfig();
break;
}

// Remove selected parts from the export
setExportContent(JSON.stringify(config, undefined, 4));
}


function updateJsonConfig() {

// Enumerate options
const options:ExportOptions[] = [];

const optionKeys:[SlCheckbox, ExportOptions][] = [
[OptionMatasIDCheckbox, ExportOptions.MatasId],
];

for (let index = 0; index < optionKeys.length; index++) {
const [checkbox, option] = optionKeys[index];
if (checkbox.checked) {
options.push(option);
}
}

// Visual: Disable options based on enabled elements
OptionMatasIDCheckbox.disabled = !SampleCheckbox.checked;
JsonSettingsPanel.classList.remove("hidden");

// Return config
return WebCTConfig.to_json({
beam: BeamCheckbox.checked,
detector: DetectorCheckbox.checked,
samples:SampleCheckbox.checked,
capture: CaptureCheckbox.checked,
recon:ReconCheckbox.checked
}, options);
}

function updateGvxrConfig() {

BeamCheckbox.disabled = true;
BeamCheckbox.checked = true;
DetectorCheckbox.disabled = true;
DetectorCheckbox.checked = true;
SampleCheckbox.checked = true;
SampleCheckbox.disabled = true;
CaptureCheckbox.checked = true;
CaptureCheckbox.disabled = true;
ReconCheckbox.checked = false;
ReconCheckbox.disabled = true;

return GVXRConfig.from_config(WebCTConfig.to_json({beam:true,detector:true,samples:true,capture:true,recon:true},[]) as configFull);
}


function updateXtekConfig() {
return {} as configSubset;
}

function setExportContent(content:string):void {
// Update code preview
CodePreview.textContent = content;
ConfigContent = content;
}

function downloadConfig():void {
// Create blob
const blob = new Blob([ConfigContent], {type:"text/plain"});

const va = document.createElement("a");
va.href = window.URL.createObjectURL(blob);

switch (ConfigFormat) {
case ExportMode.JSON:
va.download = "Config-WebCT.json";
break;
case ExportMode.GVXR:
va.download = "Config-GVXR.json";
break;
case ExportMode.XTEK:
va.download = "Config-XTEK.xtek";
break;
}

va.click();
}

function showUploadConfigDialog():void {
const fInput = document.createElement("input");
fInput.type = "file";
fInput.accept = ".json";

fInput.addEventListener("change",()=> {
console.log("Filebrowser change");

// check to see if a file was selected
if (fInput.files === undefined || fInput.files?.length != 1) {
return;
}
const file = fInput.files[0];

console.log("Loading file");

file.text().then((text) => {
console.log("Parsing file");
parseImport(text);
});
});
fInput.dispatchEvent(new MouseEvent("click"));
}

function parseImport(text:string) {
let config:configSubset | null = null;
if (text[0] == "{") {
// File format starts with a json token, try parsing and see what happens
try {
const result = JSON.parse(text);

// File format is a json format, check to see if webct or gvxr loaders will parse it.
if (GVXRConfig.can_parse(result)) {
// gvxr
console.log("Importing GVXR Config");
config = GVXRConfig.from_text(text).as_config();
console.log(config);
} else {
// webct
console.log("Importing WebCT Config");
config = WebCTConfig.parse_json(result);
console.log(config);
}
} catch (error) {
console.error(error);
// Json parse error, but first key is a {
console.error("Unparsable JSON file!");
return;
}
} else {
// Invalid file?
console.error("Unknown config filetype");
return;
}

// Assume config is now populated, any errors are early returns
if (config === null) {
return;
}

const keys = getConfigKeys(config);
// Update checkboxes to show what properties are being imported...

BeamCheckbox.checked = keys.beam;
DetectorCheckbox.checked = keys.detector;
SampleCheckbox.checked = keys.samples;
CaptureCheckbox.checked = keys.capture;
ReconCheckbox.checked = keys.recon;

console.log("Applied Config");
ConfigDialog.hide();

// apply config
WebCTConfig.apply(config);
console.log(config);
showAlert("Configuration Applied", AlertType.INFO, 5);
}
13 changes: 13 additions & 0 deletions webct/blueprints/app/static/js/formats/FormatLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { configFull, configSubset } from "../types";

// https://stackoverflow.com/questions/43723313/typescript-abstract-class-static-method-not-enforced
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface FormatLoader {
as_config: () => configSubset;
}

export interface FormatLoaderStatic {
from_config: (config:configFull) => FormatLoader;
from_text: (obj:string) => FormatLoader;
can_parse: (text:string) => boolean;
}
Loading

0 comments on commit 094fb38

Please sign in to comment.