From 8cae5704b544c7e3485ee160a0cd2a3b01f4481b Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 24 Nov 2025 07:56:12 +0100 Subject: [PATCH 1/9] See if ?preset works to load experiment remotely --- apps/class-solid/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/class-solid/README.md b/apps/class-solid/README.md index c834443..e1260eb 100644 --- a/apps/class-solid/README.md +++ b/apps/class-solid/README.md @@ -61,3 +61,25 @@ 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=`. For example to load use `http://localhost:3000/?preset=Death%20Valley`. + +## Loading experiments from url + +A saved experiment can be loaded from a url with the `preset` search query parameter. + +For example `https://classmodel.github.io/class-web?e=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 for this to work, see https://enable-cors.org/ for details. + +
+Local development + +Besides the `pnpm dev` start a static web server hosting `apps/class-solid/src/lib/presets/` directory. + +```shell +# TODO +``` + + +Visit http://localhost:3000/?preset=http://localhost:8080/death-valley.json . + +
\ No newline at end of file From e7cd062b3ced64ed17873555337e83ae6625a961 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 12:04:04 +0100 Subject: [PATCH 2/9] Only hide toast on success + allow toast of failure to be closed manually --- apps/class-solid/src/components/ui/toast.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/class-solid/src/components/ui/toast.tsx b/apps/class-solid/src/components/ui/toast.tsx index 6558a27..a05cc28 100644 --- a/apps/class-solid/src/components/ui/toast.tsx +++ b/apps/class-solid/src/components/ui/toast.tsx @@ -181,6 +181,8 @@ function showToastPromise( 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"} > {options.loading} @@ -191,6 +193,7 @@ function showToastPromise( {/* biome-ignore lint/style/noNonNullAssertion: */} {options.error?.(props.error!)} + From 8aaa7f869089d43a6de745bc556ae01d15caa31f Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 12:05:04 +0100 Subject: [PATCH 3/9] Load experiment from remote URL via `?e=` Fixes #158 --- apps/class-solid/README.md | 17 +++++++++-------- apps/class-solid/src/lib/state.ts | 25 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/class-solid/README.md b/apps/class-solid/README.md index e1260eb..0cf9b5c 100644 --- a/apps/class-solid/README.md +++ b/apps/class-solid/README.md @@ -59,27 +59,28 @@ 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=`. +An experiment from a preset can be opened from a URL like `?preset=`. For example to load use `http://localhost:3000/?preset=Death%20Valley`. -## Loading experiments from url +## Loading experiment from URL -A saved experiment can be loaded from a url with the `preset` search query parameter. +A saved experiment (`.json` file) can be loaded from a URL with the `e` search query parameter. For example `https://classmodel.github.io/class-web?e=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 for this to work, see https://enable-cors.org/ for details. +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.
Local development -Besides the `pnpm dev` start a static web server hosting `apps/class-solid/src/lib/presets/` directory. +The `./mock-wildfiredataportal/` directory contains mocked experiment similar to a wildfire at https://wildfiredataportal.eu/data/wildfire-data-portal/. + +Besides the `pnpm dev` start a static web server hosting the `./mock-wildfiredataportal/` directory. ```shell -# TODO +pnpm exec serve --cors --listen 3001 ./mock-wildfiredataportal ``` - -Visit http://localhost:3000/?preset=http://localhost:8080/death-valley.json . +Visit [http://localhost:3000/?e=http://localhost:3001/batea.json](http://localhost:3000/?e=http://localhost:3001/batea.json).
\ No newline at end of file diff --git a/apps/class-solid/src/lib/state.ts b/apps/class-solid/src/lib/state.ts index a4be0fb..3f6bc20 100644 --- a/apps/class-solid/src/lib/state.ts +++ b/apps/class-solid/src/lib/state.ts @@ -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 { @@ -44,6 +44,10 @@ export function loadFromLocalStorage() { export async function onPageLoad() { const location = useLocation(); const navigate = useNavigate(); + const experimentUrl = location.query.e; + if (experimentUrl) { + return await loadExperimentFromUrl(experimentUrl); + } const presetUrl = location.query.preset; if (presetUrl) { return await loadExperimentPreset(presetUrl); @@ -112,3 +116,22 @@ export function saveToLocalStorage() { duration: 1000, }); } + +async function loadExperimentFromUrl(url: string) { + const navigate = useNavigate(); + showToastPromise( + async () => { + const response = await fetch(url); + const rawData = await response.json(); + await uploadExperiment(rawData); + // clear ?e from URL after loading, as any edits would make URL a lie + navigate("/"); + }, + { + loading: "Loading experiment from URL...", + success: () => "Experiment loaded from URL", + error: (error) => `Failed to load experiment from URL: ${error}`, + duration: 1000, + }, + ); +} From 9e02c299f3e113c9513d5dafc5b0eee530322a9f Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 12:15:51 +0100 Subject: [PATCH 4/9] Embed json file into README --- apps/class-solid/README.md | 59 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/apps/class-solid/README.md b/apps/class-solid/README.md index 0cf9b5c..62bd02f 100644 --- a/apps/class-solid/README.md +++ b/apps/class-solid/README.md @@ -73,11 +73,66 @@ The server hosting the JSON file must have CORS enabled so the CLASS web applica
Local development -The `./mock-wildfiredataportal/` directory contains mocked experiment similar to a wildfire at https://wildfiredataportal.eu/data/wildfire-data-portal/. - Besides the `pnpm dev` start a static web server hosting the `./mock-wildfiredataportal/` directory. ```shell +mkdir -p ./mock-wildfiredataportal +# Create a mocked experiment similar to https://wildfiredataportal.eu/fire/batea/ +cat < ./mock-wildfiredataportal/batea.json +{ + "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 ``` From c2dfb7618a23ecbaf9f0edf172a2a7e7573dddf5 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 12:20:37 +0100 Subject: [PATCH 5/9] Do not try to parse non-200 responses --- apps/class-solid/src/lib/state.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/class-solid/src/lib/state.ts b/apps/class-solid/src/lib/state.ts index 3f6bc20..9ec4313 100644 --- a/apps/class-solid/src/lib/state.ts +++ b/apps/class-solid/src/lib/state.ts @@ -122,6 +122,11 @@ async function loadExperimentFromUrl(url: string) { 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.json(); await uploadExperiment(rawData); // clear ?e from URL after loading, as any edits would make URL a lie From 50b3064f705727ad8534244df2dc36fedcda4a1a Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 12:34:22 +0100 Subject: [PATCH 6/9] Plot pressure, temperature and relative humidity from observations Fixes #184 --- apps/class-solid/src/lib/profiles.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/class-solid/src/lib/profiles.ts b/apps/class-solid/src/lib/profiles.ts index a0c9d17..7cd969e 100644 --- a/apps/class-solid/src/lib/profiles.ts +++ b/apps/class-solid/src/lib/profiles.ts @@ -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 }; } From edd84c2e5993c8af04301b0ad7bff2a53340cba2 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 13:46:45 +0100 Subject: [PATCH 7/9] If shareable link is too large give hosting state file as alternative --- apps/class-solid/README.md | 104 +++++++++--------- .../src/components/ShareButton.tsx | 75 +++++++++++-- apps/class-solid/src/lib/state.ts | 12 +- 3 files changed, 124 insertions(+), 67 deletions(-) diff --git a/apps/class-solid/README.md b/apps/class-solid/README.md index 62bd02f..29c957c 100644 --- a/apps/class-solid/README.md +++ b/apps/class-solid/README.md @@ -80,62 +80,64 @@ mkdir -p ./mock-wildfiredataportal # Create a mocked experiment similar to https://wildfiredataportal.eu/fire/batea/ cat < ./mock-wildfiredataportal/batea.json { - "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] - } - ] + "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/?e=http://localhost:3001/batea.json](http://localhost:3000/?e=http://localhost:3001/batea.json). +Visit [http://localhost:3000/?s=http://localhost:3001/batea.json](http://localhost:3000/?s=http://localhost:3001/batea.json).
\ No newline at end of file diff --git a/apps/class-solid/src/components/ShareButton.tsx b/apps/class-solid/src/components/ShareButton.tsx index 7285db3..25b7f85 100644 --- a/apps/class-solid/src/components/ShareButton.tsx +++ b/apps/class-solid/src/components/ShareButton.tsx @@ -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"; @@ -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 { @@ -72,11 +88,50 @@ export function ShareButton() { - Cannot share application state, it is too large. Please download - each experiment by itself or make it smaller by removing - permutations and/or experiments. -

+ <> +

+ Cannot embed application state in shareable link, it is too + large. +

+

+ Alternativly you can create your own shareable link by hosting + the state remotely: +

+
    +
  1. + + Download state + {" "} + as file +
  2. +
  3. + Upload the state file to some static hosting service like your + own web server or an AWS S3 bucket. +
  4. +
  5. + Open the CLASS web application with + "https://classmodel.github.io/class-web?s=<your remote + url>". +
  6. +
+

+ Make sure the CLASS web application is{" "} + + allowed to download from remote location + + . +

+ } > { @@ -127,8 +127,8 @@ async function loadExperimentFromUrl(url: string) { `Failed to download experiment from ${url}: ${response.status} ${response.statusText}`, ); } - const rawData = await response.json(); - await uploadExperiment(rawData); + const rawData = await response.text(); + await loadStateFromString(rawData); // clear ?e from URL after loading, as any edits would make URL a lie navigate("/"); }, From 36be4741ca565125075efb9d7544f99382dd4edd Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 13:52:30 +0100 Subject: [PATCH 8/9] Always clear adress bar from `?s=...` --- apps/class-solid/src/lib/state.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/class-solid/src/lib/state.ts b/apps/class-solid/src/lib/state.ts index 0183231..e2ab646 100644 --- a/apps/class-solid/src/lib/state.ts +++ b/apps/class-solid/src/lib/state.ts @@ -46,7 +46,11 @@ export async function onPageLoad() { const navigate = useNavigate(); const stateUrl = location.query.s; if (stateUrl) { - return await loadStateFromURL(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) { @@ -118,8 +122,7 @@ export function saveToLocalStorage() { } async function loadStateFromURL(url: string) { - const navigate = useNavigate(); - showToastPromise( + await showToastPromise( async () => { const response = await fetch(url); if (!response.ok) { @@ -129,8 +132,6 @@ async function loadStateFromURL(url: string) { } const rawData = await response.text(); await loadStateFromString(rawData); - // clear ?e from URL after loading, as any edits would make URL a lie - navigate("/"); }, { loading: "Loading experiment from URL...", From 9d0c94df2f69879c5a14dc04f56d98f2306637d1 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 24 Nov 2025 13:57:50 +0100 Subject: [PATCH 9/9] more e to s replacements + spelling --- apps/class-solid/README.md | 6 +++--- apps/class-solid/src/components/ShareButton.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/class-solid/README.md b/apps/class-solid/README.md index 29c957c..54b5387 100644 --- a/apps/class-solid/README.md +++ b/apps/class-solid/README.md @@ -64,9 +64,9 @@ For example to load use `http://localhost:30 ## Loading experiment from URL -A saved experiment (`.json` file) can be loaded from a URL with the `e` search query parameter. +A saved state (`class-.json` file) can be loaded from a URL with the `s` search query parameter. -For example `https://classmodel.github.io/class-web?e=https://wildfiredataportal.eu/fire/batea/class.json` will load the experiment from `https://wildfiredataportal.eu/fire/batea/class.json`. +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. @@ -77,7 +77,7 @@ Besides the `pnpm dev` start a static web server hosting the `./mock-wildfiredat ```shell mkdir -p ./mock-wildfiredataportal -# Create a mocked experiment similar to https://wildfiredataportal.eu/fire/batea/ +# Create a mocked state with experiment similar to https://wildfiredataportal.eu/fire/batea/ cat < ./mock-wildfiredataportal/batea.json { "experiments": [{ diff --git a/apps/class-solid/src/components/ShareButton.tsx b/apps/class-solid/src/components/ShareButton.tsx index 25b7f85..8816f0c 100644 --- a/apps/class-solid/src/components/ShareButton.tsx +++ b/apps/class-solid/src/components/ShareButton.tsx @@ -94,7 +94,7 @@ export function ShareButton() { large.

- Alternativly you can create your own shareable link by hosting + Alternatively you can create your own shareable link by hosting the state remotely: