code cleanup and restructuring
This commit is contained in:
parent
a680f2813e
commit
d730695936
|
@ -0,0 +1,2 @@
|
||||||
|
# Base url of your wxbox-tiler instance
|
||||||
|
PUBLIC_TILER_URL_BASE=""
|
|
@ -1,84 +0,0 @@
|
||||||
<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 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ActionReturn } from 'svelte/action';
|
||||||
|
import type { TileLayer, Map as LeafletMap } from 'leaflet';
|
||||||
|
import { tilerLayerAttribution, tilerLayerUrl } from '$lib/map/layer';
|
||||||
|
import type * as Leaflet 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 }: Props = $props();
|
||||||
|
|
||||||
|
let mapContainerElement: HTMLElement;
|
||||||
|
// await import('leaflet') done at runtime
|
||||||
|
let L: Leaflet | null = $state(null);
|
||||||
|
// Base layer - openstreetmap, carto, etc
|
||||||
|
let layer0: TileLayer;
|
||||||
|
// Data layer - composite reflectivity, velocity (the actual data)
|
||||||
|
let layer1: TileLayer;
|
||||||
|
|
||||||
|
// Layer0 (base) updating
|
||||||
|
$effect(() => {
|
||||||
|
// if leaflet hasn't been imported yet, skip
|
||||||
|
// this also sets a dependency, so we'll be re-ran once it has
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// if there is already a layer0 and a map, remove the old one
|
||||||
|
if (layer0 && map) {
|
||||||
|
layer0.removeFrom(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenStreetMap
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer1 (data) updating
|
||||||
|
$effect(() => {
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// remove existing layer1, if there is one
|
||||||
|
if (layer1 && map) {
|
||||||
|
layer1.removeFrom(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLayer) {
|
||||||
|
layer1 = L.tileLayer(tilerLayerUrl(dataLayer), {
|
||||||
|
attribution: tilerLayerAttribution(dataLayer)
|
||||||
|
});
|
||||||
|
layer1.addTo(map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ran when the div below (see use:mapAction) is created
|
||||||
|
async function mapAction(): Promise<ActionReturn> {
|
||||||
|
// dynamically imports leaflet, as it's a browser lib
|
||||||
|
L = await import('leaflet');
|
||||||
|
// imports leaflet.sync, for syncing
|
||||||
|
await import('leaflet.sync');
|
||||||
|
|
||||||
|
// create the map
|
||||||
|
map = L.map(mapContainerElement, {
|
||||||
|
// geo center of CONUS
|
||||||
|
center: [39.83, -98.583],
|
||||||
|
zoom: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!map) return {};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="map" class:mapselected={selected} bind:this={mapContainerElement} 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>
|
|
@ -0,0 +1,2 @@
|
||||||
|
// There is no good-looking three-pane view
|
||||||
|
export type View = 'one' | 'two' | 'four';
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
|
export interface MutexLayerSet<ValidOpts> {
|
||||||
|
map1: ValidOpts | null;
|
||||||
|
map2: ValidOpts | null;
|
||||||
|
map3: ValidOpts | null;
|
||||||
|
map4: ValidOpts | null;
|
||||||
|
}
|
||||||
|
export interface OverlayLayerSet<ValidOpts> {
|
||||||
|
map1: ValidOpts[];
|
||||||
|
map2: ValidOpts[];
|
||||||
|
map3: ValidOpts[];
|
||||||
|
map4: ValidOpts[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseLayers = 'osm';
|
||||||
|
export type DataLayers = 'noaa_mrms_merged_composite_reflectivity_qc';
|
||||||
|
|
||||||
|
export function tilerLayerUrl(id: DataLayers): string {
|
||||||
|
if (!env.PUBLIC_TILER_URL_BASE) {
|
||||||
|
throw new Error('PUBLIC_TILER_URL_BASE env var not set!');
|
||||||
|
}
|
||||||
|
const base = new URL(env.PUBLIC_TILER_URL_BASE);
|
||||||
|
return (base + `${id}/{z}/{x}/{y}.png`).toString();
|
||||||
|
}
|
||||||
|
export function tilerLayerAttribution(id: DataLayers): string {
|
||||||
|
let base;
|
||||||
|
if (id === 'noaa_mrms_merged_composite_reflectivity_qc') {
|
||||||
|
base = '© NOAA';
|
||||||
|
}
|
||||||
|
return base + ', © wxbox';
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import type { Map as LMap } from 'leaflet';
|
||||||
|
import type { View } from '$lib/map';
|
||||||
|
|
||||||
|
export function syncMaps(
|
||||||
|
view: View,
|
||||||
|
map1: LMap | null,
|
||||||
|
map2: LMap | null,
|
||||||
|
map3: LMap | null,
|
||||||
|
map4: LMap | null
|
||||||
|
) {
|
||||||
|
// resize the shown maps
|
||||||
|
if (view === 'one' && map1) {
|
||||||
|
map1.invalidateSize();
|
||||||
|
} else if (view === 'two') {
|
||||||
|
if (map1) map1.invalidateSize();
|
||||||
|
if (map2) map2.invalidateSize();
|
||||||
|
} else if (view === 'four') {
|
||||||
|
if (map1) map1.invalidateSize();
|
||||||
|
if (map2) map2.invalidateSize();
|
||||||
|
if (map3) map3.invalidateSize();
|
||||||
|
if (map4) map4.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map1 && map2) {
|
||||||
|
map2.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map2);
|
||||||
|
}
|
||||||
|
if (map1 && map3) {
|
||||||
|
map3.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map3);
|
||||||
|
}
|
||||||
|
if (map1 && map4) {
|
||||||
|
map4.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map1) map2.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map3) map2.sync(map3);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map4) map2.sync(map4);
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map1) map3.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map2) map3.sync(map2);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map4) map3.sync(map4);
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map1) map4.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map2) map4.sync(map2);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map3) map4.sync(map3);
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { MenuItem } from '$lib/menubar/buttonlib';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menu: MenuItem[];
|
||||||
|
}
|
||||||
|
let { menu }: Props = $props();
|
||||||
|
|
||||||
|
function key(e: KeyboardEvent) {
|
||||||
|
let k = e.key;
|
||||||
|
for (let menuItem of menu) {
|
||||||
|
if (k === menuItem.keyboard) {
|
||||||
|
menuItem.action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={key} />
|
||||||
|
|
||||||
|
{#each menu 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}
|
|
@ -0,0 +1,22 @@
|
||||||
|
export type Mode =
|
||||||
|
| 'global'
|
||||||
|
| 'view'
|
||||||
|
| 'paneSelect'
|
||||||
|
| 'pane'
|
||||||
|
| 'baseLayerSelect'
|
||||||
|
| 'dataLayerSelect'
|
||||||
|
| 'overlayLayerSelect'
|
||||||
|
| 'dataNOAA'
|
||||||
|
| 'dataNOAAMRMS';
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
display: string;
|
||||||
|
keyboard: string;
|
||||||
|
disabled: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuRegistry = {
|
||||||
|
[id in Mode]: MenuItem[];
|
||||||
|
};
|
|
@ -1,12 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet;
|
||||||
}
|
}
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
@ -1,270 +1,383 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Map from "$lib/Map.svelte";
|
import Map from '$lib/map/Map.svelte';
|
||||||
|
import type { MenuRegistry, Mode } from '$lib/menubar/buttonlib';
|
||||||
|
import ButtonBar from '$lib/menubar/ButtonBar.svelte';
|
||||||
|
import type { BaseLayers, DataLayers, MutexLayerSet, OverlayLayerSet } from '$lib/map/layer';
|
||||||
|
import type { View } from '$lib/map';
|
||||||
|
import type { Map as LMap } from 'leaflet';
|
||||||
|
import { syncMaps } from '$lib/map/sync.svelte.js';
|
||||||
|
|
||||||
let map1 = $state(null);
|
let map1: LMap | null = $state(null);
|
||||||
let map2 = $state(null);
|
let map2: LMap | null = $state(null);
|
||||||
let map3 = $state(null);
|
let map3: LMap | null = $state(null);
|
||||||
let map4 = $state(null);
|
let map4: LMap | null = $state(null);
|
||||||
|
|
||||||
let view: "one" | "two" | "four" = $state("one");
|
let view: View = $state('one');
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!map1) return;
|
syncMaps(view, map1, map2, map3, map4);
|
||||||
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(() => {
|
let pane: 'map1' | 'map2' | 'map3' | 'map4' = $state('map1');
|
||||||
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);
|
let baseLayer: MutexLayerSet<BaseLayers> = $state({
|
||||||
if (map2 && map3) map2.sync(map3);
|
map1: 'osm',
|
||||||
if (map2 && map4) map2.sync(map4);
|
map2: 'osm',
|
||||||
|
map3: 'osm',
|
||||||
|
map4: 'osm'
|
||||||
|
});
|
||||||
|
let dataLayer: MutexLayerSet<DataLayers | null> = $state({
|
||||||
|
map1: null,
|
||||||
|
map2: null,
|
||||||
|
map3: null,
|
||||||
|
map4: null
|
||||||
|
});
|
||||||
|
let overlayLayers: OverlayLayerSet<string[]> = $state({
|
||||||
|
map1: [],
|
||||||
|
map2: [],
|
||||||
|
map3: [],
|
||||||
|
map4: []
|
||||||
|
});
|
||||||
|
|
||||||
if (map3 && map1) map3.sync(map1);
|
let mode: Mode = $state('global');
|
||||||
if (map3 && map2) map3.sync(map2);
|
let registry: MenuRegistry = $derived({
|
||||||
if (map3 && map4) map3.sync(map4);
|
global: [
|
||||||
|
{
|
||||||
|
display: 'view',
|
||||||
|
keyboard: 'v',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'view';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane',
|
||||||
|
keyboard: 'p',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'paneSelect';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
view: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
paneSelect: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pane: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
baseLayerSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'OpenStreetMap',
|
||||||
|
keyboard: 'o',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
baseLayer[pane] = 'osm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataLayerSelect: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataNOAA: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataNOAAMRMS: [
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
overlayLayerSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
if (map4 && map1) map4.sync(map1);
|
let status: string = $derived.by(() => {
|
||||||
if (map4 && map2) map4.sync(map2);
|
return mode + ' ' + pane;
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={key} />
|
|
||||||
|
|
||||||
<div class="outercontainer">
|
<div class="outercontainer">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h1>wxbox</h1>
|
<h1>wxbox</h1>
|
||||||
<span>{status}</span>
|
<span>{status}</span>
|
||||||
{#each currentMenu as menuItem}
|
<ButtonBar menu={registry[mode]} />
|
||||||
{#if menuItem.visible}
|
</div>
|
||||||
{@const index = menuItem.display.indexOf(menuItem.keyboard)}
|
<div class="container">
|
||||||
<button disabled={menuItem.disabled} onclick={menuItem.action}>
|
<Map
|
||||||
{#if index !== -1}
|
selected={pane === 'map1'}
|
||||||
{menuItem.display.substring(0, index)}<u>{menuItem.display.charAt(index)}</u>{menuItem.display.substring(index+1)}
|
bind:map={map1}
|
||||||
{:else}
|
baseLayer={baseLayer.map1}
|
||||||
{menuItem.display} (<u>{menuItem.keyboard}</u>)
|
dataLayer={dataLayer.map1}
|
||||||
{/if}
|
overlayLayers={overlayLayers.map1}
|
||||||
</button>
|
/>
|
||||||
{/if}
|
{#if view === 'two' || view === 'four'}
|
||||||
{/each}
|
<Map
|
||||||
</div>
|
selected={pane === 'map2'}
|
||||||
<div class="container">
|
bind:map={map2}
|
||||||
<Map selected={pane === "map1" && (paneModes.includes(mode))} bind:map={map1} baseLayer={baseLayer.map1} dataLayer={dataLayer.map1} overlayLayers={overlayLayers.map1} />
|
baseLayer={baseLayer.map2}
|
||||||
{#if view === "two" || view === "four"}
|
dataLayer={dataLayer.map2}
|
||||||
<Map selected={pane === "map2" && (paneModes.includes(mode))} bind:map={map2} baseLayer={baseLayer.map2} dataLayer={dataLayer.map2} overlayLayers={overlayLayers.map2} />
|
overlayLayers={overlayLayers.map2}
|
||||||
{/if}
|
/>
|
||||||
</div>
|
{/if}
|
||||||
{#if view === "four"}
|
</div>
|
||||||
<div class="container">
|
{#if view === 'four'}
|
||||||
<Map selected={pane === "map3" && (paneModes.includes(mode))} bind:map={map3} baseLayer={baseLayer.map3} dataLayer={dataLayer.map3} overlayLayers={overlayLayers.map3} />
|
<div class="container">
|
||||||
<Map selected={pane === "map4" && (paneModes.includes(mode))} bind:map={map4} baseLayer={baseLayer.map4} dataLayer={dataLayer.map4} overlayLayers={overlayLayers.map4} />
|
<Map
|
||||||
</div>
|
selected={pane === 'map3'}
|
||||||
{/if}
|
bind:map={map3}
|
||||||
<div class="footer">
|
baseLayer={baseLayer.map3}
|
||||||
<p>built with <3</p>
|
dataLayer={dataLayer.map3}
|
||||||
<p>coredoes.dev :)</p>
|
overlayLayers={overlayLayers.map3}
|
||||||
</div>
|
/>
|
||||||
|
<Map
|
||||||
|
selected={pane === 'map4'}
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toolbar {
|
.toolbar {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.toolbar h1 {
|
.toolbar h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
:global(html body) {
|
:global(html body) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.outercontainer {
|
.outercontainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.footer p {
|
.footer p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue