Started working on optimization result view.

This commit is contained in:
Daan Vanden Bosch 2019-09-09 18:37:20 +02:00
parent d7490b0d3c
commit 0cfa20e30f
12 changed files with 119 additions and 213 deletions

View File

@ -11,7 +11,7 @@ import "./Table.css";
export type Column<T> = {
title: string;
sticky?: boolean;
width?: number;
width: number;
text_align?: string;
create_cell(value: T, disposer: Disposer): HTMLTableCellElement;
};
@ -47,10 +47,10 @@ export class Table<T> extends Widget<HTMLTableElement> {
if (column.sticky) {
th.style.position = "sticky";
th.style.left = `${left}px`;
left += column.width || 0;
left += column.width;
}
if (column.width != undefined) th.style.width = `${column.width}px`;
th.style.width = `${column.width}px`;
return th;
}),
@ -84,7 +84,10 @@ export class Table<T> extends Widget<HTMLTableElement> {
this.tbody_element.append(...rows);
} else {
for (let i = 0; i < amount; i++) {
this.tbody_element.children[index + i].insertAdjacentElement("afterend", rows[i]);
this.tbody_element.children[index + i].insertAdjacentElement(
"beforebegin",
rows[i],
);
}
}
};

View File

@ -63,10 +63,7 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
this.extract_observables = extract_observables;
}
observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void,
options?: { call_now?: true },
): Disposable {
observe_list(observer: (change: ListPropertyChangeEvent<T>) => void): Disposable {
if (this.value_observers.length === 0 && this.extract_observables) {
this.replace_element_observers(0, Infinity, this.values);
}
@ -75,15 +72,6 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
this.list_observers.push(observer);
}
if (options && options.call_now) {
this.call_list_observer(observer, {
type: ListChangeType.ListChange,
index: 0,
removed: [],
inserted: this.values,
});
}
return {
dispose: () => {
const index = this.list_observers.indexOf(observer);

View File

@ -1,37 +0,0 @@
import { Property } from "../Property";
import { LoadableState } from "./LoadableState";
/**
* Represents a value that can be loaded asynchronously.
* [state]{@link LoadableProperty#state} represents the current state of this Loadable's value.
*/
export interface LoadableProperty<T> extends Property<T> {
readonly state: Property<LoadableState>;
/**
* True if the initial data load has happened. It may or may not have succeeded.
* Check [error]{@link LoadableProperty#error} to know whether an error occurred.
*/
readonly is_initialized: Property<boolean>;
/**
* True if a data load is underway. This may be the initializing load or a later reload.
*/
readonly is_loading: Property<boolean>;
/**
* This property returns valid data as soon as possible.
* If the Loadable is uninitialized a data load will be triggered, otherwise the current value will be returned.
*/
readonly promise: Promise<T>;
/**
* Contains the {@link Error} object if an error occurred during the most recent data load.
*/
readonly error: Property<Error | undefined>;
/**
* Load the data. Initializes the Loadable if it is uninitialized.
*/
load(): Promise<T>;
}

View File

@ -1,26 +0,0 @@
export enum LoadableState {
/**
* No attempt has been made to load data.
*/
Uninitialized,
/**
* The first data load is underway.
*/
Initializing,
/**
* Data was loaded at least once. The most recent load was successful.
*/
Nominal,
/**
* Data was loaded at least once. The most recent load failed.
*/
Error,
/**
* Data was loaded at least once. Another data load is underway.
*/
Reloading,
}

View File

@ -1,84 +0,0 @@
import { Property } from "../Property";
import { WritableProperty } from "../WritableProperty";
import { property } from "../../index";
import { AbstractProperty } from "../AbstractProperty";
import { LoadableState } from "./LoadableState";
import { LoadableProperty } from "./LoadableProperty";
export class SimpleLoadableProperty<T> extends AbstractProperty<T> implements LoadableProperty<T> {
get val(): T {
return this.get_val();
}
readonly state: Property<LoadableState>;
readonly is_initialized: Property<boolean>;
readonly is_loading: Property<boolean>;
get promise(): Promise<T> {
// Load value on first use.
if (this._state.val === LoadableState.Uninitialized) {
return this.load_value();
} else {
return this._promise;
}
}
readonly error: Property<Error | undefined>;
private _val: T;
private _promise: Promise<T>;
private readonly _state: WritableProperty<LoadableState> = property(
LoadableState.Uninitialized,
);
private readonly _load?: () => Promise<T>;
private readonly _error: WritableProperty<Error | undefined> = property(undefined);
constructor(initial_value: T, load?: () => Promise<T>) {
super();
this._val = initial_value;
this._promise = new Promise(resolve => resolve(this._val));
this.state = this._state;
this.is_initialized = this.state.map(state => state !== LoadableState.Uninitialized);
this.is_loading = this.state.map(
state => state === LoadableState.Initializing || state === LoadableState.Reloading,
);
this._load = load;
this.error = this._error;
}
get_val(): T {
return this._val;
}
load(): Promise<T> {
return this.load_value();
}
private async load_value(): Promise<T> {
if (this.is_loading.val) return this._promise;
this._state.val = LoadableState.Initializing;
const old_val = this._val;
try {
if (this._load) {
this._promise = this._load();
this._val = await this._promise;
}
this._state.val = LoadableState.Nominal;
this._error.val = undefined;
return this._val;
} catch (e) {
this._state.val = LoadableState.Error;
this._error.val = e;
throw e;
} finally {
this.emit(old_val);
}
}
}

View File

@ -1,41 +0,0 @@
import { LoadableState } from "./LoadableState";
import { Property } from "../Property";
import { WritableProperty } from "../WritableProperty";
import { property } from "../../index";
export class Store {
readonly state: Property<LoadableState>;
/**
* True if the initial data load has happened. It may or may not have succeeded.
* Check [error]{@link LoadableProperty#error} to know whether an error occurred.
*/
readonly is_initialized: Property<boolean>;
/**
* True if a data load is underway. This may be the initializing load or a later reload.
*/
readonly is_loading: Property<boolean>;
/**
* Contains the {@link Error} object if an error occurred during the most recent data load.
*/
readonly error: Property<Error | undefined>;
private readonly _state: WritableProperty<LoadableState> = property(
LoadableState.Uninitialized,
);
private readonly _error: WritableProperty<Error | undefined> = property(undefined);
constructor() {
this.state = this._state;
this.is_initialized = this.state.map(state => state !== LoadableState.Uninitialized);
this.is_loading = this.state.map(
state => state === LoadableState.Initializing || state === LoadableState.Reloading,
);
this.error = this._error;
}
}

View File

@ -0,0 +1,17 @@
import { sequential } from "./sequential";
test("sequential functions should run sequentially", () => {
let time = 10;
const f = sequential(() => new Promise(resolve => setTimeout(resolve, time--)));
const resolved_values: number[] = [];
let last_promise!: Promise<any>;
for (let i = 0; i < 10; i++) {
last_promise = f().then(() => resolved_values.push(i));
}
expect(resolved_values).toEqual([]);
return last_promise.then(() => expect(resolved_values).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]));
});

View File

@ -2,7 +2,7 @@ import { Server } from "../model";
import { Property } from "../observable/property/Property";
import { gui_store } from "./GuiStore";
import { memoize } from "lodash";
import { sequential } from "../util";
import { sequential } from "../sequential";
import { Disposable } from "../observable/Disposable";
/**

View File

@ -0,0 +1,87 @@
import { Widget } from "../../core/gui/Widget";
import { el } from "../../core/gui/dom";
import { Table } from "../../core/gui/Table";
import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore";
import { Disposable } from "../../core/observable/Disposable";
import { list_property } from "../../core/observable";
import { OptimalMethodModel } from "../model";
import { Difficulty } from "../../core/model";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
export class OptimizationResultView extends Widget {
private results_observer?: Disposable;
constructor() {
super(
el.div(
{ class: "hunt_optimizer_OptimizationResultView" },
el.h2({ text: "Optimization Result" }),
),
);
const optimal_methods = list_property<OptimalMethodModel>();
this.element.append(
this.disposable(
new Table({
values: optimal_methods,
columns: [
{
title: "Difficulty",
width: 80,
create_cell(value: OptimalMethodModel) {
return el.td({ text: Difficulty[value.difficulty] });
},
},
{
title: "Method",
width: 200,
create_cell(value: OptimalMethodModel) {
return el.td({ text: value.method_name });
},
},
{
title: "Ep.",
width: 50,
create_cell(value: OptimalMethodModel) {
return el.td({ text: Episode[value.method_episode] });
},
},
],
}),
).element,
);
this.disposable(
hunt_optimizer_stores.observe_current(
hunt_optimizer_store => {
if (this.results_observer) {
this.results_observer.dispose();
}
this.results_observer = hunt_optimizer_store.result.observe(
({ value: result }) => {
if (result) {
optimal_methods.val = result.optimal_methods;
} else {
optimal_methods.val = [];
}
},
{
call_now: true,
},
);
},
{ call_now: true },
),
);
}
dispose(): void {
super.dispose();
if (this.results_observer) {
this.results_observer.dispose();
}
}
}

View File

@ -2,11 +2,15 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { WantedItemsView } from "./WantedItemsView";
import "./OptimizerView.css";
import { OptimizationResultView } from "./OptimizationResultView";
export class OptimizerView extends ResizableWidget {
constructor() {
super(el.div({ class: "hunt_optimizer_OptimizerView" }));
this.element.append(this.disposable(new WantedItemsView()).element);
this.element.append(
this.disposable(new WantedItemsView()).element,
this.disposable(new OptimizationResultView()).element,
);
}
}

View File

@ -9,7 +9,6 @@ import {
} from "../../core/observable/property/list/ListProperty";
import { WantedItemModel } from "../model";
import { NumberInput } from "../../core/gui/NumberInput";
import { ToolBar } from "../../core/gui/ToolBar";
import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore";
import { Disposable } from "../../core/observable/Disposable";
@ -23,7 +22,6 @@ export class WantedItemsView extends Widget {
this.element.append(
el.h2({ text: "Wanted Items" }),
this.disposable(new ToolBar({ children: [new Button("Optimize")] })).element,
el.div(
{ class: "hunt_optimizer_WantedItemsView_table_wrapper" },
el.table({}, this.tbody_element),
@ -39,9 +37,6 @@ export class WantedItemsView extends Widget {
this.wanted_items_observer = hunt_optimizer_store.wanted_items.observe_list(
this.update_table,
{
call_now: true,
},
);
},
{ call_now: true },
@ -70,7 +65,7 @@ export class WantedItemsView extends Widget {
} else {
for (let i = 0; i < change.inserted.length; i++) {
this.tbody_element.children[change.index + i].insertAdjacentElement(
"afterend",
"beforebegin",
rows[i],
);
}