From 8bbd39fec2ceca9998fed5e777700af4bd7eb799 Mon Sep 17 00:00:00 2001 From: core <core@coredoes.dev> Date: Sat, 26 Oct 2024 22:41:12 -0400 Subject: [PATCH] web client - command system --- bun.lockb | Bin 0 -> 1244 bytes package.json | 1 + wxbox-web/src/lib/Map.svelte | 84 ++++++++++ wxbox-web/src/routes/+page.svelte | 269 +++++++++++++++++++++++++++--- 4 files changed, 329 insertions(+), 25 deletions(-) create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 wxbox-web/src/lib/Map.svelte diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..bded5171cab60b91783595cbf62273e6e64274e9 GIT binary patch literal 1244 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p+nO}g%L zdqtK^`8-AHF8{^-OHAJI?YW=s{Ac3h`Sb5jvh`sBDgpwKS`grXP;j~d%CCYcfbt;} z4+8^C0fT=$&zzFPn`;<RRFKYrSp+nj!2xPMNDS&wq6`GN0ofg>{D1uakR^#Sk$@tg zd5jDYbCHc^sh;mt$m}B$<{6>Dc3!ct=xAzbdP$+*vG-Bi(&VC^e7pDV*$(Lqy^e=A zKM!Ki{mXmi%|!7%I<GjM&%H4<%8mCovS9?AK&mw9MjwDi%@rukWm8;~te2TrT#}fR zqX$cXdLc!rsdfrR1`5TQRjK)DItnHVi8-0+dHHEvFgO1D4*?)P*d5Fe85StbWm9Tw zWCzrU!w3VYIujz)gUru>s%wI)k2W%kEwunT(+qBhA%-&<K<OKnu9={|W?(o2H3dea zhC^{`Zdq!Po@+%(YEfQdPH;(4W?s6Tf+4~p3%Er;peDIM6F1P`KsOm;by7}hVp>jW ziC%GKUUEiBNkOrdzJ5_^dS-D+QKep9L2g#DUVc%!KC&`>T@<dafu50`iC#&16(qC= G9|-_y?Y;#7 literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a3d797 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{ "dependencies": { "leaflet.sync": "^0.2.4" } } \ No newline at end of file diff --git a/wxbox-web/src/lib/Map.svelte b/wxbox-web/src/lib/Map.svelte new file mode 100644 index 0000000..65384b3 --- /dev/null +++ b/wxbox-web/src/lib/Map.svelte @@ -0,0 +1,84 @@ +<script lang="ts"> + import type {ActionReturn} from "svelte/action"; + import type {TileLayer, Map as LeafletMap} from "leaflet"; + + interface Props { + map: LeafletMap | null, + selected: boolean, + baseLayer: "osm", + dataLayer: "noaa_mrms_merged_composite_reflectivity_qc" | null, + overlayLayers: string[] + } + let { map = $bindable(null), selected, baseLayer, dataLayer, overlayLayers } = $props(); + + let mapEl: HTMLElement; + let L; + let layer0: TileLayer; + let layer1: TileLayer; + + $inspect(dataLayer); + + $effect(() => { + console.log("layer0", layer0, map, baseLayer); + if (!L) return; + + if (layer0 && map) { + layer0.removeFrom(map); + } + + if (baseLayer === "osm") { + layer0 = L.tileLayer( + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + { + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + } + ); + layer0.addTo(map); + } + }); + + $effect(() => { + console.log("layer1", layer1, map, dataLayer); + if (!L) return; + + if (layer1 && map) { + layer1.removeFrom(map); + } + + if (dataLayer === "noaa_mrms_merged_composite_reflectivity_qc") { + layer1 = L.tileLayer( + 'http://localhost:8080/noaa_mrms_merged_composite_reflectivity_qc/{z}/{x}/{y}.png', + { + attribution: '© NOAA, © wxbox' + } + ); + layer1.addTo(map); + } + }); + + async function mapAction(node: HTMLElement): Promise<ActionReturn> { + L = await import('leaflet'); + await import("leaflet.sync"); + + map = L.map(mapEl, { + center: [39.83, -98.583], + zoom: 5 + }); + + if (!map) return {}; + } +</script> + +<div class="map" class:mapselected={selected} bind:this={mapEl} use:mapAction></div> + +<style> + .map { + flex: 1; + box-sizing: border-box; + border: 2px solid transparent; + } + .mapselected { + box-sizing: border-box; + border: 2px solid red; + } +</style> \ No newline at end of file diff --git a/wxbox-web/src/routes/+page.svelte b/wxbox-web/src/routes/+page.svelte index bcd4402..2acedca 100644 --- a/wxbox-web/src/routes/+page.svelte +++ b/wxbox-web/src/routes/+page.svelte @@ -1,51 +1,270 @@ <script lang="ts"> - import type {ActionReturn} from "svelte/action"; - import type {TileLayer, Map as LeafletMap} from "leaflet"; + import Map from "$lib/Map.svelte"; - let mapEl: HTMLElement; - let map: LeafletMap; - let L; + let map1 = $state(null); + let map2 = $state(null); + let map3 = $state(null); + let map4 = $state(null); - async function mapAction(node: HTMLElement): ActionReturn { - L = await import('leaflet'); + let view: "one" | "two" | "four" = $state("one"); - map = L.map(mapEl, { - center: [51.505, -0.09], - zoom: 13 - }); + $effect(() => { + if (!map1) return; + if (!map2) return; + if (!map3) return; + if (!map4) return; + // resize the shown maps + if (view === "one") { + map1.invalidateSize(); + } else if (view === "two") { + map1.invalidateSize(); + map2.invalidateSize(); + } else if (view === "four") { + map1.invalidateSize(); + map2.invalidateSize(); + map3.invalidateSize(); + map4.invalidateSize(); + } + }) - L.control.layers( - { - "OpenStreetMap": L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "©" }) - }, - { - "NOAA/MRMS CONUS Reflectivity at Lowest Altitude": L.tileLayer("http://localhost:8080/noaa_mrms_merged_composite_reflectivity_qc/{z}/{x}/{y}.png", { attribution: "© NOAA" }) - } - ).addTo(map); + $effect(() => { + if (map1 && map2) { + map2.setView(map1.getCenter(), map1.getZoom()); + map1.sync(map2); + } + if (map1 && map3) { + map3.setView(map1.getCenter(), map1.getZoom()); + map1.sync(map3); + } + if (map1 && map4) { + map4.setView(map1.getCenter(), map1.getZoom()); + map1.sync(map4); + } - return { - destroy: () => { + if (map2 && map1) map2.sync(map1); + if (map2 && map3) map2.sync(map3); + if (map2 && map4) map2.sync(map4); + if (map3 && map1) map3.sync(map1); + if (map3 && map2) map3.sync(map2); + if (map3 && map4) map3.sync(map4); + + if (map4 && map1) map4.sync(map1); + if (map4 && map2) map4.sync(map2); + if (map4 && map3) map4.sync(map3); + }); + + let mode: "global" | "view" | "paneSelect" | "pane" | "baseLayerSelect" | "dataLayerSelect" | "overlayLayerSelect" | "dataNOAA" | "dataNOAAMRMS" = $state("global"); + let pane: "map1" | "map2" | "map3" | "map4" = $state("map1"); + + interface MenuItem { + display: string, + keyboard: string, + disabled: boolean, + visible: boolean, + action: () => void + } + + let globalMenu: MenuItem[] = $derived([ + { display: "view", keyboard: "v", disabled: false, visible: true, action: () => { mode = "view" } }, + { display: "pane", keyboard: "p", disabled: false, visible: true, action: () => { mode = "paneSelect"} }, + ]); + let viewMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "global" }}, + { display: "view: 1", keyboard: "1", disabled: view === "one", visible: true, action: () => { view = "one" }}, + { display: "view: 2", keyboard: "2", disabled: view === "two", visible: true, action: () => { view = "two" }}, + { display: "view: 4", keyboard: "4", disabled: view === "four", visible: true, action: () => { view = "four" }} + ]); + let paneSelectMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "global" }}, + { display: "pane: 1", keyboard: "1", disabled: pane === "map1", visible: true, action: () => { pane = "map1" }}, + { display: "pane: 2", keyboard: "2", disabled: pane === "map2", visible: view === "two" || view === "four", action: () => { pane = "map2" }}, + { display: "pane: 3", keyboard: "3", disabled: pane === "map3", visible: view === "four", action: () => { pane = "map3" }}, + { display: "pane: 4", keyboard: "4", disabled: pane === "map4", visible: view === "four", action: () => { pane = "map4" }}, + { display: "select", keyboard: "Enter", disabled: false, visible: true, action: () => { mode = "pane" }}, + ]); + let paneMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "paneSelect" }}, + { display: "base layer", keyboard: "b", disabled: false, visible: true, action: () => { mode = "baseLayerSelect" }}, + { display: "data layer", keyboard: "d", disabled: false, visible: true, action: () => { mode = "dataLayerSelect" }}, + { display: "overlays", keyboard: "o", disabled: false, visible: true, action: () => { mode = "overlayLayerSelect" }}, + ]); + + interface MutexLayerSet<ValidOpts> { + map1: ValidOpts | null, + map2: ValidOpts | null, + map3: ValidOpts | null, + map4: ValidOpts | null + } + interface OverlayLayerSet<ValidOpts> { + map1: ValidOpts[], + map2: ValidOpts[], + map3: ValidOpts[], + map4: ValidOpts[] + } + + let baseLayer: MutexLayerSet<"osm"> = $state({ + map1: "osm", + map2: "osm", + map3: "osm", + map4: "osm" + }); + let dataLayer: MutexLayerSet<"noaa_mrms_merged_composite_reflectivity_qc" | null> = $state({ + map1: null, + map2: null, + map3: null, + map4: null + }); + let overlayLayers: OverlayLayerSet<string[]> = $state({ + map1: [], + map2: [], + map3: [], + map4: [] + }); + + $inspect(dataLayer); + + let baseLayerMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "pane" }}, + { display: "OpenStreetMap", keyboard: "o", disabled: false, visible: true, action: () => { + baseLayer[pane] = "osm"; + }}, + ]); + let dataLayerMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "pane" }}, + { display: "none", keyboard: "0", disabled: false, visible: true, action: () => { dataLayer[pane] = null }}, + { display: "noaa", keyboard: "n", disabled: false, visible: true, action: () => { mode = "dataNOAA" }}, + ]); + let dataNOAAMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "dataLayerSelect" }}, + { display: "multi-radar multi-sensor", keyboard: "m", disabled: false, visible: true, action: () => { mode = "dataNOAAMRMS" }}, + ]); + let dataNOAAMRMSMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "dataNOAA" }}, + { display: "composite reflectivity - merged qc", keyboard: "r", disabled: dataLayer[pane] === "noaa_mrms_merged_composite_reflectivity_qc", visible: true, action: () => { + dataLayer[pane] = "noaa_mrms_merged_composite_reflectivity_qc"; + }} + ]); + let overlayLayerMenu: MenuItem[] = $derived([ + { display: "back", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "pane" }}, + ]); + + let currentMenu: MenuItem[] = $derived.by(() => { + if (mode === "global") { + return globalMenu; + } else if (mode === "view") { + return viewMenu; + } else if (mode === "paneSelect") { + return paneSelectMenu; + } else if (mode === "pane") { + return paneMenu; + } else if (mode === "baseLayerSelect") { + return baseLayerMenu; + } else if (mode === "dataLayerSelect") { + return dataLayerMenu; + } else if (mode === "overlayLayerSelect") { + return overlayLayerMenu; + } else if (mode === "dataNOAA") { + return dataNOAAMenu; + } else if (mode === "dataNOAAMRMS") { + return dataNOAAMRMSMenu; + } else { + return [ + { display: "Invalid submenu :( go back to root", keyboard: "Escape", disabled: false, visible: true, action: () => { mode = "global" }} + ] + } + }); + + let paneModes = ["paneSelect", "pane", "baseLayerSelect", "dataLayerSelect", "overlayLayerSelect", "dataNOAA", "dataNOAAMRMS"]; + + let status: string = $derived.by(() => { + let s = mode; + + if (paneModes.includes(mode)) { + s += " " + pane; + } + + return s; + }) + + function key(e: KeyboardEvent) { + let k = e.key; + for (let menuItem of currentMenu) { + if (k === menuItem.keyboard) { + menuItem.action(); + return; } } } </script> -<div class="container"> - <div class="map" bind:this={mapEl} use:mapAction></div> +<svelte:window onkeydown={key} /> + +<div class="outercontainer"> + <div class="toolbar"> + <h1>wxbox</h1> + <span>{status}</span> + {#each currentMenu as menuItem} + {#if menuItem.visible} + {@const index = menuItem.display.indexOf(menuItem.keyboard)} + <button disabled={menuItem.disabled} onclick={menuItem.action}> + {#if index !== -1} + {menuItem.display.substring(0, index)}<u>{menuItem.display.charAt(index)}</u>{menuItem.display.substring(index+1)} + {:else} + {menuItem.display} (<u>{menuItem.keyboard}</u>) + {/if} + </button> + {/if} + {/each} + </div> + <div class="container"> + <Map selected={pane === "map1" && (paneModes.includes(mode))} bind:map={map1} baseLayer={baseLayer.map1} dataLayer={dataLayer.map1} overlayLayers={overlayLayers.map1} /> + {#if view === "two" || view === "four"} + <Map selected={pane === "map2" && (paneModes.includes(mode))} bind:map={map2} baseLayer={baseLayer.map2} dataLayer={dataLayer.map2} overlayLayers={overlayLayers.map2} /> + {/if} + </div> + {#if view === "four"} + <div class="container"> + <Map selected={pane === "map3" && (paneModes.includes(mode))} bind:map={map3} baseLayer={baseLayer.map3} dataLayer={dataLayer.map3} overlayLayers={overlayLayers.map3} /> + <Map selected={pane === "map4" && (paneModes.includes(mode))} bind:map={map4} baseLayer={baseLayer.map4} dataLayer={dataLayer.map4} overlayLayers={overlayLayers.map4} /> + </div> + {/if} + <div class="footer"> + <p>built with <3</p> + <p>coredoes.dev :)</p> + </div> </div> <style> + .toolbar { + margin: 0; + font-size: 12px; + display: flex; + flex-direction: row; + } + .toolbar h1 { + margin: 0; + } :global(html body) { margin: 0; padding: 0; } .container { display: flex; - flex-direction: column; + flex-direction: row; + flex: 1; } - .map { + .outercontainer { + display: flex; + flex-direction: column; width: 100vw; height: 100vh; } + .footer { + display: flex; + justify-content: space-between; + } + .footer p { + margin: 0; + } </style> \ No newline at end of file