diff --git a/client/src/client/internal.ts b/client/src/client/internal.ts index a8a95553..6e6a8f79 100644 --- a/client/src/client/internal.ts +++ b/client/src/client/internal.ts @@ -23,14 +23,35 @@ export const internal: Dictionary = { 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 {}; + } } }, }; diff --git a/client/src/components/app-bar/FeaturePicker.tsx b/client/src/components/app-bar/FeaturePicker.tsx index 66082156..464ff501 100644 --- a/client/src/components/app-bar/FeaturePicker.tsx +++ b/client/src/components/app-bar/FeaturePicker.tsx @@ -23,7 +23,7 @@ export function FeaturePicker({ label, value, onChange, items, icon }: Props) { {selected?.name ?? label} )} - items={map(items, ({ id, name, description }) => ({ + items={map(items, ({ id, name, description, hidden }) => ({ value: id, label: ( <> @@ -34,6 +34,7 @@ export function FeaturePicker({ label, value, onChange, items, icon }: Props) { ), + disabled: hidden, }))} value={selected?.id} onChange={onChange} diff --git a/client/src/components/app-bar/Input.tsx b/client/src/components/app-bar/Input.tsx index fcb77f09..810383be 100644 --- a/client/src/components/app-bar/Input.tsx +++ b/client/src/components/app-bar/Input.tsx @@ -1,12 +1,17 @@ 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 }; @@ -14,7 +19,7 @@ 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 ( <> @@ -23,7 +28,8 @@ 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, @@ -31,16 +37,45 @@ export function Input() { ]} 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; } }} @@ -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: {} })} /> ); diff --git a/client/src/components/app-bar/upload.tsx b/client/src/components/app-bar/upload.tsx index b4ca0d75..1435636f 100644 --- a/client/src/components/app-bar/upload.tsx +++ b/client/src/components/app-bar/upload.tsx @@ -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(); @@ -11,13 +12,47 @@ function name(s: string) { const customMapId = "internal/custom"; +const customTraceId = "json"; + export const custom = (map?: Partial) => ({ - 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, @@ -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.`); } diff --git a/client/src/components/generic/Select.tsx b/client/src/components/generic/Select.tsx index 1b676f11..84c405c1 100644 --- a/client/src/components/generic/Select.tsx +++ b/client/src/components/generic/Select.tsx @@ -14,7 +14,7 @@ type Key = string | number; export type SelectProps = { trigger?: (props: ReturnType) => ReactElement; - items?: { value: T; label?: ReactNode }[]; + items?: { value: T; label?: ReactNode; disabled?: boolean }[]; value?: T; onChange?: (value: T) => void; placeholder?: string; @@ -50,8 +50,9 @@ export function Select({ horizontal: "center", }} > - {map(items, ({ value: v, label }) => ( + {map(items, ({ value: v, label, disabled }) => ( ( (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 { diff --git a/client/src/components/renderer/index.tsx b/client/src/components/renderer/index.tsx index 8ec6ebcf..54f3c819 100644 --- a/client/src/components/renderer/index.tsx +++ b/client/src/components/renderer/index.tsx @@ -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 = "") { diff --git a/client/src/services/SpecimenService.tsx b/client/src/services/SpecimenService.tsx index dd853517..46c4369f 100644 --- a/client/src/services/SpecimenService.tsx +++ b/client/src/services/SpecimenService.tsx @@ -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"; @@ -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 }; } } @@ -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) ? ( -