web client - command system
This commit is contained in:
parent
4df7ac4724
commit
8bbd39fec2
|
@ -0,0 +1 @@
|
|||
{ "dependencies": { "leaflet.sync": "^0.2.4" } }
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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" })
|
||||
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
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
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>
|
||||
|
||||
<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">
|
||||
<div class="map" bind:this={mapEl} use:mapAction></div>
|
||||
<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>
|
Loading…
Reference in New Issue