Skip to content

Commit

Permalink
Add ability to upload search trace and map
Browse files Browse the repository at this point in the history
  • Loading branch information
spaaaacccee committed Apr 24, 2023
1 parent bf9890f commit c67aa67
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 60 deletions.
31 changes: 26 additions & 5 deletions client/src/client/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,35 @@ export const internal: Dictionary<Transport["call"]> = {
case "features/formats":
return [
{
id: "json",
name: "Search Trace",
id: "grid",
name: "Grid",
},
{
id: "xy",
name: "Network",
},
{
id: "mesh",
name: "Mesh",
},
];
case "features/algorithms":
return [
{
id: "identity",
name: "Unknown",
hidden: true,
},
];
case "solve/pathfinding":
const { mapURI } = (params as PathfindingTask["params"])!;
const { scheme, content } = parseURI(mapURI);
if (["map:", "trace:"].includes(scheme)) return JSON.parse(content);
const { parameters } = (params as PathfindingTask<{
content?: string;
}>["params"])!;
try {
return JSON.parse(parameters?.content ?? "");
} catch {
return {};
}
}
},
};
3 changes: 2 additions & 1 deletion client/src/components/app-bar/FeaturePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function FeaturePicker({ label, value, onChange, items, icon }: Props) {
{selected?.name ?? label}
</Button>
)}
items={map(items, ({ id, name, description }) => ({
items={map(items, ({ id, name, description, hidden }) => ({
value: id,
label: (
<>
Expand All @@ -34,6 +34,7 @@ export function FeaturePicker({ label, value, onChange, items, icon }: Props) {
</Type>
</>
),
disabled: hidden,
}))}
value={selected?.id}
onChange={onChange}
Expand Down
53 changes: 44 additions & 9 deletions client/src/components/app-bar/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Code as CodeIcon, MapTwoTone as MapIcon } from "@material-ui/icons";
import { useSnackbar } from "components/generic/Snackbar";
import { Space } from "components/generic/Space";
import { find } from "lodash";
import { find, merge } from "lodash";
import { useUIState } from "slices/UIState";
import { useConnections } from "slices/connections";
import { useFeatures } from "slices/features";
import { useUIState } from "slices/UIState";
import { FeaturePicker } from "./FeaturePicker";
import { custom, upload } from "./upload";
import {
custom as customMap,
customTrace,
uploadMap,
uploadTrace,
} from "./upload";

export const mapDefaults = { start: undefined, end: undefined };

export function Input() {
const notify = useSnackbar();
const [connections] = useConnections();
const [{ algorithms, maps, formats }] = useFeatures();
const [{ algorithm, map }, setUIState] = useUIState();
const [{ algorithm, map, parameters }, setUIState] = useUIState();

return (
<>
Expand All @@ -23,24 +28,54 @@ export function Input() {
label="map"
value={map?.id}
items={[
custom(map),
customTrace(parameters),
customMap(map),
...maps.map((c) => ({
...c,
description: find(connections, { url: c.source })?.name,
})),
]}
onChange={async (v) => {
switch (v) {
case custom().id:
case customMap().id:
try {
const f = await uploadMap(formats);
if (f) {
setUIState({
...mapDefaults,
map: f,
algorithm: algorithm ?? "identity",
parameters: {},
});
notify("Solution was cleared because the map changed.");
}
} catch (e) {
notify(`${e}`);
}
break;
case customTrace().id:
try {
const f = await upload(formats);
if (f) setUIState({ ...mapDefaults, map: f });
const f2 = await uploadTrace();
if (f2) {
setUIState({
parameters: f2,
algorithm: "identity",
start: 0,
end: 0,
map: {
format: f2.format,
content: map?.format === f2.format ? map?.content : " ",
id: "internal/upload",
},
});
}
} catch (e) {
notify(`${e}`);
}
break;
default:
setUIState({ ...mapDefaults, map: find(maps, { id: v }) });
notify("Solution was cleared because the map changed.");
break;
}
}}
Expand All @@ -54,7 +89,7 @@ export function Input() {
...c,
description: find(connections, { url: c.source })?.name,
}))}
onChange={(v) => setUIState({ algorithm: v })}
onChange={async (v) => setUIState({ algorithm: v, parameters: {} })}
/>
</>
);
Expand Down
43 changes: 39 additions & 4 deletions client/src/components/app-bar/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fileDialog as file } from "file-select-dialog";
import { find, startCase } from "lodash";
import { Feature, FeatureDescriptor } from "protocol/FeatureQuery";
import { Parameters } from "protocol/SolveTask";

function ext(s: string) {
return s.split(".").pop();
Expand All @@ -11,13 +12,47 @@ function name(s: string) {

const customMapId = "internal/custom";

const customTraceId = "json";

export const custom = (map?: Partial<Feature>) => ({
name: map?.id === customMapId ? `Custom - ${map?.name}` : "Custom",
description: "Import Map",
name: map?.id === customMapId ? `Imported Map - ${map?.name}` : "Import Map",
description: "Internal",
id: customMapId,
});

export async function upload(accept: FeatureDescriptor[]) {
export const customTrace = (trace?: Parameters) => ({
name:
trace?.type === customTraceId
? `Imported Trace - ${trace?.name}`
: "Import Trace",
description: "Internal",
id: customTraceId,
});

const TRACE_FORMAT = "json";

export async function uploadTrace() {
const f = await file({
accept: [`.${TRACE_FORMAT}`],
strict: true,
});
if (f) {
if (ext(f.name) === TRACE_FORMAT) {
const content = await f.text();
return {
...customTrace(),
format: JSON.parse(content)?.format,
content,
name: startCase(name(f.name)),
type: customTraceId,
} as Parameters;
} else {
throw new Error(`The format (${ext(f.name)}) is unsupported.`);
}
}
}

export async function uploadMap(accept: FeatureDescriptor[]) {
const f = await file({
accept: accept.map(({ id }) => `.${id}`),
strict: true,
Expand All @@ -29,7 +64,7 @@ export async function upload(accept: FeatureDescriptor[]) {
format: ext(f.name),
content: await f.text(),
name: startCase(name(f.name)),
} as Feature;
} as Feature & { format?: string };
} else {
throw new Error(`The format (${ext(f.name)}) is unsupported.`);
}
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/generic/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Key = string | number;

export type SelectProps<T extends Key> = {
trigger?: (props: ReturnType<typeof bindTrigger>) => ReactElement;
items?: { value: T; label?: ReactNode }[];
items?: { value: T; label?: ReactNode; disabled?: boolean }[];
value?: T;
onChange?: (value: T) => void;
placeholder?: string;
Expand Down Expand Up @@ -50,8 +50,9 @@ export function Select<T extends string>({
horizontal: "center",
}}
>
{map(items, ({ value: v, label }) => (
{map(items, ({ value: v, label, disabled }) => (
<MenuItem
disabled={disabled}
key={v}
value={v}
selected={v === value}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/renderer/grid/parse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Options = {
export const parse = makeMapParser<Options, Structure>(
(m, { wall = "@" }: Options) => {
const lines = m.split("\n");
const [, h, w, , ...grid] = lines;
const [, h = "", w = "", , ...grid] = lines;
const [width, height] = [w, h].map((d) => +last(d.split(" "))!);

return {
Expand Down
12 changes: 11 additions & 1 deletion client/src/components/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ import { DefaultRenderer } from "./default";
import { GridRenderer } from "./grid";
import { MeshRenderer } from "./mesh";
import { NetworkRenderer } from "./network";
import { RendererMap } from "./Renderer";
import { RendererMap, RendererProps } from "./Renderer";
import { useSpecimen } from "slices/specimen";
import { useUIState } from "slices/UIState";
import { createElement } from "react";

export function JSONRenderer(props: RendererProps) {
const [{ specimen }] = useSpecimen();
const [{ parameters }] = useUIState();
return createElement(renderers[parameters?.format], props);
}

const renderers: RendererMap = {
grid: GridRenderer,
xy: NetworkRenderer,
mesh: MeshRenderer,
json: JSONRenderer,
};

export function getRenderer(key = "") {
Expand Down
80 changes: 45 additions & 35 deletions client/src/services/SpecimenService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { find, isEmpty } from "lodash";
import { ParamsOf } from "protocol/Message";
import { PathfindingTask } from "protocol/SolveTask";
import { useAsyncAbortable as useAsync } from "react-async-hook";
import { useConnections } from "slices/connections";
import { useFeatures } from "slices/features";
import { useLoadingState } from "slices/loading";
import { Specimen, useSpecimen } from "slices/specimen";
Expand All @@ -33,7 +34,8 @@ async function solve(
map,
format: specimen?.format ?? format,
};
} catch (e: any) {
} catch (e) {
///@ts-ignore
return { ...p, specimen: {}, map, format, error: e.message };
}
}
Expand All @@ -43,50 +45,57 @@ async function solve(
export function SpecimenService() {
const usingLoadingState = useLoadingState("specimen");
const notify = useSnackbar();
const [{ formats: format }] = useFeatures();
const [{ algorithm, start, end }, setUIState] = useUIState();
const [{ formats, algorithms }] = useFeatures();
const [{ algorithm, start, end, parameters }, setUIState] = useUIState();
const resolve = useConnectionResolver();
const [connections] = useConnections();
const [, setSpecimen] = useSpecimen();

const { result: map } = useMapContent();

useAsync(
(signal) =>
usingLoadingState(async () => {
if (map?.format && map?.content) {
const entry = find(format, { id: map.format });
let entry;
for (const connection of connections) {
const a = await connection.call("features/algorithms");
const f = await connection.call("features/formats");
if (find(a, { id: algorithm }) && find(f, { id: map?.format })) {
entry = connection;
break;
}
}
if (entry) {
const connection = resolve({ url: entry.source });
if (connection) {
const solution = await solve(
map.content,
{
algorithm,
format: map.format,
instances: [{ end, start }],
},
connection.call
);
if (solution && !signal.aborted) {
setSpecimen(solution);
setUIState({ step: 0, playback: "paused", breakpoints: [] });
notify(
solution.error ??
(!isEmpty(solution.specimen) ? (
<Label
primary="Solution generated."
secondary={`${solution.specimen?.eventList?.length} steps`}
/>
) : (
"Ready."
))
);
}
} else
const solution = await solve(
map?.content ?? "",
{
algorithm,
format: map?.format ?? "",
instances: [{ end, start }],
parameters,
},
entry.call
);
if (solution && !signal.aborted) {
setSpecimen(solution);
setUIState({ step: 0, playback: "paused", breakpoints: [] });
notify(
`No solver is available for the map format (${map.format}).`
solution.error ??
(!isEmpty(solution.specimen) ? (
<Label
primary="Solution generated."
secondary={`${solution.specimen?.eventList?.length} steps`}
/>
) : (
"Ready."
))
);
}
}
} else
notify(
`No solver is available for the map format (${map?.format ??
"none"}) and algorithm (${algorithm ?? "none"}).`
);
}
}),
[
Expand All @@ -96,9 +105,10 @@ export function SpecimenService() {
map,
notify,
usingLoadingState,
format,
formats,
resolve,
setSpecimen,
parameters,
]
);

Expand Down
Loading

0 comments on commit c67aa67

Please sign in to comment.