Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion apps/class-solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,85 @@ The format is JSON with content adhering to the [JSON schema](https://github.com
The `src/lib/presets.ts` is used as an index of presets.
If you add a preset the `src/lib/presets.ts` file needs to be updated.

An experiment from a preset can be opened from a url like `?preset=<preset-name>`.
An experiment from a preset can be opened from a URL like `?preset=<preset-name>`.
For example to load <src/lib/presets/death-valley.json> use `http://localhost:3000/?preset=Death%20Valley`.

## Loading experiment from URL

A saved state (`class-<experiment-name>.json` file) can be loaded from a URL with the `s` search query parameter.

For example `https://classmodel.github.io/class-web?s=https://wildfiredataportal.eu/fire/batea/class.json` will load the experiment from `https://wildfiredataportal.eu/fire/batea/class.json`.

The server hosting the JSON file must have CORS enabled so the CLASS web application is allowed to download it, see [https://enable-cors.org](https://enable-cors.org) for details.

<details>
<summary>Local development</summary>

Besides the `pnpm dev` start a static web server hosting the `./mock-wildfiredataportal/` directory.

```shell
mkdir -p ./mock-wildfiredataportal
# Create a mocked state with experiment similar to https://wildfiredataportal.eu/fire/batea/
cat <<EOF > ./mock-wildfiredataportal/batea.json
{
"experiments": [{
"reference": {
"name": "batea",
"description": "Copied from https://wildfiredataportal.eu/fire/batea/ with mocked observations.",
"h": 912,
"theta": 299.1,
"dtheta": 0.816,
"gamma_theta": [0.00509, 0.00216],
"z_theta": [2138, 4000],
"qt": 0.0055,
"dqt": -0.000826,
"gamma_qt": [-8.08e-7, -5.62e-7],
"z_qt": [2253, 4000],
"divU": -6.7e-7,
"u": -3.22,
"ug": -1.9,
"du": 1.33,
"gamma_u": [0.00186, 0.00404],
"z_u": [2125, 4000],
"v": 4.81,
"vg": 5.81,
"dv": 1,
"gamma_v": [-0.00243, -0.001],
"z_v": [1200, 4000],
"ustar": 0.1,
"runtime": 10800,
"wtheta": [0.404, 0.41, 0.375, 0.308, 0.205, 0.12, 0.036, 0, 0, 0, 0, 0],
"wq": [
0, 0, 0, 0, 7.6e-7, 0.00000128, 0.00000146, 0.00000125, 0.00000115,
0.00000115, 0.00000252, 0.00000183
],
"fc": 0.000096,
"p0": 97431,
"z0m": 0.45,
"z0h": 0.00281,
"is_tuned": true,
"t0": "2024-05-11T12:00:00Z"
},
"preset": "Varnavas",
"permutations": [],
"observations": [
{
"name": "Mocked soundings",
"height": [0, 1000, 2000, 3000, 4000],
"pressure": [900, 800, 700, 600, 500],
"temperature": [16.4, 10.2, 4.0, -2.2, -8.4],
"relativeHumidity": [30, 25, 20, 15, 10],
"windSpeed": [2, 5, 10, 15, 20],
"windDirection": [180, 200, 220, 240, 260]
}
]
}]
}
EOF

pnpm exec serve --cors --listen 3001 ./mock-wildfiredataportal
```

Visit [http://localhost:3000/?s=http://localhost:3001/batea.json](http://localhost:3000/?s=http://localhost:3001/batea.json).

</details>
75 changes: 65 additions & 10 deletions apps/class-solid/src/components/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Show, createMemo, createSignal } from "solid-js";
import { Show, createMemo, createSignal, onCleanup } from "solid-js";
import { Button } from "~/components/ui/button";
import { encodeAppState } from "~/lib/encode";
import { analyses, experiments } from "~/lib/store";
Expand All @@ -23,18 +23,34 @@ export function ShareButton() {
const [open, setOpen] = createSignal(false);
const [isCopied, setIsCopied] = createSignal(false);
let inputRef: HTMLInputElement | undefined;
const shareableLink = createMemo(() => {
const encodedAppState = createMemo(() => {
if (!open()) {
return "";
}

const appState = encodeAppState(experiments, analyses);
return encodeAppState(experiments, analyses);
});
const shareableLink = createMemo(() => {
const basePath = import.meta.env.DEV
? ""
: import.meta.env.BASE_URL.replace("/_build", "");
const url = `${window.location.origin}${basePath}#${appState}`;
const url = `${window.location.origin}${basePath}#${encodedAppState()}`;
return url;
});
const downloadUrl = createMemo(() => {
return URL.createObjectURL(
new Blob([decodeURI(encodedAppState())], {
type: "application/json",
}),
);
});
onCleanup(() => {
URL.revokeObjectURL(downloadUrl());
});

const filename = createMemo(() => {
const names = experiments.map((e) => e.config.reference.name).join("-");
return `class-${names.slice(0, 120)}.json`;
});

async function copyToClipboard() {
try {
Expand Down Expand Up @@ -72,11 +88,50 @@ export function ShareButton() {
<Show
when={shareableLink().length < MAX_SHAREABLE_LINK_LENGTH}
fallback={
<p>
Cannot share application state, it is too large. Please download
each experiment by itself or make it smaller by removing
permutations and/or experiments.
</p>
<>
<p>
Cannot embed application state in shareable link, it is too
large.
</p>
<p>
Alternatively you can create your own shareable link by hosting
the state remotely:
</p>
<ol class="list-inside list-decimal space-y-1">
<li>
<a
class="underline"
href={downloadUrl()}
download={filename()}
type="application/json"
>
Download state
</a>{" "}
as file
</li>
<li>
Upload the state file to some static hosting service like your
own web server or an AWS S3 bucket.
</li>
<li>
Open the CLASS web application with
"https://classmodel.github.io/class-web?s=&lt;your remote
url&gt;".
</li>
</ol>
<p>
Make sure the CLASS web application is{" "}
<a
href="https://enable-cors.org/server.html"
target="_blank"
rel="noreferrer"
class="underline"
>
allowed to download from remote location
</a>
.
</p>
</>
}
>
<Show
Expand Down
3 changes: 3 additions & 0 deletions apps/class-solid/src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ function showToastPromise<T, U>(
toastId={props.toastId}
variant={variant[props.state]}
duration={options.duration}
// Only hide toast after duration if it's in success state
persistent={props.state !== "fulfilled"}
>
<Switch>
<Match when={props.state === "pending"}>{options.loading}</Match>
Expand All @@ -191,6 +193,7 @@ function showToastPromise<T, U>(
<Match when={props.state === "rejected"}>
{/* biome-ignore lint/style/noNonNullAssertion: <explanation> */}
{options.error?.(props.error!)}
<ToastClose />
</Match>
</Switch>
Comment thread
sverhoeven marked this conversation as resolved.
</Toast>
Expand Down
8 changes: 7 additions & 1 deletion apps/class-solid/src/lib/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,15 @@ export function observationsForProfile(obs: Observation, variable = "theta") {
return { y: h, x: u };
case "v":
return { y: h, x: v };
case "rh":
return { y: h, x: rh };
case "T":
return { y: h, x: T };
case "p":
return { y: h, x: p };
default:
console.warn(
"Unknown variable '${variable}' for observation profile.",
`Unknown variable '${variable}' for observation profile.`,
);
return { y: Number.NaN, x: Number.NaN };
}
Expand Down
31 changes: 30 additions & 1 deletion apps/class-solid/src/lib/state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useLocation, useNavigate } from "@solidjs/router";
import { showToast } from "~/components/ui/toast";
import { showToast, showToastPromise } from "~/components/ui/toast";
import { encodeAppState } from "./encode";
import { findPresetByName } from "./presets";
import {
Expand Down Expand Up @@ -44,6 +44,14 @@ export function loadFromLocalStorage() {
export async function onPageLoad() {
const location = useLocation();
const navigate = useNavigate();
const stateUrl = location.query.s;
if (stateUrl) {
await loadStateFromURL(stateUrl);
// Remove query parameter after loading state from URL,
// as after editing the experiment the URL gets out of sync
navigate("/");
return;
}
const presetUrl = location.query.preset;
if (presetUrl) {
return await loadExperimentPreset(presetUrl);
Expand Down Expand Up @@ -112,3 +120,24 @@ export function saveToLocalStorage() {
duration: 1000,
});
}

async function loadStateFromURL(url: string) {
await showToastPromise(
async () => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to download experiment from ${url}: ${response.status} ${response.statusText}`,
);
}
const rawData = await response.text();
await loadStateFromString(rawData);
},
{
loading: "Loading experiment from URL...",
success: () => "Experiment loaded from URL",
error: (error) => `Failed to load experiment from URL: ${error}`,
duration: 1000,
},
);
}