diff --git a/package-lock.json b/package-lock.json index 90b5453a..c006747b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,10 +25,12 @@ "@vitest/coverage-v8": "^4.0.17", "bits-ui": "^2.15.2", "clsx": "^2.1.1", + "crepuscule": "file:../../forks/crepuscule", "eslint": "^9.39.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.13.0", "globals": "^17.3.0", + "mapbox-gl-shadow-simulator": "^0.67.0", "maplibre-gl": "^5.17.0", "mdsvex": "^0.12.3", "mode-watcher": "^1.1.0", @@ -52,6 +54,26 @@ "vitest-browser-svelte": "^2.0.1" } }, + "../../forks/crepuscule": { + "version": "1.0.0", + "dev": true, + "dependencies": { + "maplibre-gl": "^5.17.0" + }, + "devDependencies": { + "@types/node": "^20.4.5", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", + "eslint": "^8.44.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.28.0", + "prettier": "^2.8.8", + "typescript": "^5.0.4", + "vite": "^4.4.8", + "vite-plugin-dts": "^3.4.0", + "vite-plugin-plain-text": "^1.4.2" + } + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -2797,6 +2819,10 @@ "node": ">= 0.6" } }, + "node_modules/crepuscule": { + "resolved": "../../forks/crepuscule", + "link": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4048,6 +4074,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mapbox-gl-shadow-simulator": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/mapbox-gl-shadow-simulator/-/mapbox-gl-shadow-simulator-0.67.0.tgz", + "integrity": "sha512-XycKUZB8FDbYZE068CmOX4UAZeKhh8N7OQpnVCbdoyn9YEzM6BE9nHZ2iiz8DbRq5zX6taHICg94l44H6RRMNg==", + "dev": true, + "license": "UNLICENSED", + "dependencies": { + "suncalc": "^1.9.0" + } + }, "node_modules/maplibre-gl": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.17.0.tgz", @@ -5005,6 +5041,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/suncalc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/suncalc/-/suncalc-1.9.0.tgz", + "integrity": "sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A==", + "dev": true + }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", diff --git a/package.json b/package.json index 7e80b063..6a024103 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,12 @@ "@vitest/coverage-v8": "^4.0.17", "bits-ui": "^2.15.2", "clsx": "^2.1.1", + "crepuscule": "file:../../forks/crepuscule", "eslint": "^9.39.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.13.0", "globals": "^17.3.0", + "mapbox-gl-shadow-simulator": "^0.67.0", "maplibre-gl": "^5.17.0", "mdsvex": "^0.12.3", "mode-watcher": "^1.1.0", diff --git a/src/lib/components/scale/scale.svelte b/src/lib/components/scale/scale.svelte index dcbc3bfd..1d7e1d7e 100644 --- a/src/lib/components/scale/scale.svelte +++ b/src/lib/components/scale/scale.svelte @@ -98,10 +98,8 @@ : 'bottom-2.5'} duration-500 left-2.5 z-10 select-none" style="max-height: {totalHeight + 100}px;" > -
-
+
+
{#each labeledColors as lc, i (lc)} {@const alphaValue = getAlpha(lc.color)} - + No domains found. @@ -342,9 +340,7 @@ > - + No variables found. @@ -454,9 +450,7 @@ > - + No levels found. diff --git a/src/lib/components/time/time-selector.svelte b/src/lib/components/time/time-selector.svelte index ae247f68..189ce379 100644 --- a/src/lib/components/time/time-selector.svelte +++ b/src/lib/components/time/time-selector.svelte @@ -8,9 +8,16 @@ import { browser } from '$app/environment'; - import { desktop, loading, preferences } from '$lib/stores/preferences'; - import { metaJson, modelRunLocked } from '$lib/stores/time'; - import { inProgress, latest, modelRun, now, time } from '$lib/stores/time'; + import { desktop, loading, preferences, shadeMap } from '$lib/stores/preferences'; + import { + inProgress, + latest, + metaJson, + modelRun, + modelRunLocked, + now, + time + } from '$lib/stores/time'; import { domainSelectionOpen, selectedDomain, @@ -452,19 +459,23 @@ // generates all possible time steps for the current day const timeStepsComplete = $derived.by(() => { - const timeStepsComplete = []; - for (let day of daySteps) { - for (let i = 0; i <= 23; i++) { - if (metaFirstResolutionHours === 0.25) { - for (let j = 0; j < 60; j += 15) { - timeStepsComplete.push(withLocalTime(day, i, j)); + if (metaFirstResolutionHours) { + const timeStepsComplete = []; + for (let day of daySteps) { + for (let i = 0; i <= 23; i++) { + if (metaFirstResolutionHours === 0.25) { + for (let j = 0; j < 60; j += 15) { + timeStepsComplete.push(withLocalTime(day, i, j)); + } + } else { + timeStepsComplete.push(withLocalTime(day, i)); } - } else { - timeStepsComplete.push(withLocalTime(day, i)); } } + return timeStepsComplete; + } else { + return undefined; } - return timeStepsComplete; }); // state variables for mouse interaction and scrolling behavior @@ -476,7 +487,7 @@ let isScrolling = $state(false); const centerDateButton = (date: Date, smooth = true) => { - if (dayContainer) { + if (dayContainer && timeStepsComplete) { const index = timeStepsComplete.findIndex((tSC) => tSC.getTime() === date.getTime()); if (index !== -1) { if (desktop.current) { @@ -521,11 +532,18 @@ let hoveredHour = $derived( timeStepsComplete - ? timeStepsComplete[ - Math.round( - (timeStepsComplete.length * (hoverX + dayContainerScrollLeft)) / dayContainerScrollWidth - ) - ] + ? // ? timeStepsComplete[ + // Math.round( + // (timeStepsComplete.length * + // ) + // ] + new Date( + timeStepsComplete[0].getTime() + + ((timeStepsComplete[timeStepsComplete.length - 1].getTime() - + timeStepsComplete[0].getTime()) * + (hoverX + dayContainerScrollLeft)) / + dayContainerScrollWidth + ) : metaFirstTime ); @@ -558,14 +576,17 @@ onMount(() => { if (hoursHoverContainer) { hoursHoverContainer.addEventListener('mousemove', (e) => { - if (hoursHoverContainerWidth) + if (hoursHoverContainerWidth) { hoverX = e.layerX + (isSafari ? hoursHoverContainerWidth / 2 : 0); + $shadeMap?.setDate(hoveredHour); + } }); hoursHoverContainer.addEventListener('mouseout', () => { hoverX = 0; + $shadeMap?.setDate($time); }); hoursHoverContainer.addEventListener('click', () => { - if (desktop.current) { + if (desktop.current && timeStepsComplete) { let validTime = false; let timeStep = timeStepsComplete[ @@ -628,7 +649,7 @@ }); const onScrollEvent = (e: Event) => { - if (isScrolling) return; + if (isScrolling || !timeStepsComplete) return; const target = e.target as Element; const left = target.scrollLeft; @@ -636,12 +657,21 @@ if (left === 0) { currentDate.setHours(0); } - let timeStep = - timeStepsComplete[ - Math.round( - (timeStepsComplete.length * target.scrollLeft) / (dayContainerScrollWidth - viewWidth) - ) - ]; + // let timeStep = + // timeStepsComplete[ + // Math.round( + // (timeStepsComplete.length * target.scrollLeft) / (dayContainerScrollWidth - viewWidth) + // ) + // ]; + // + let timeStep = new Date( + timeStepsComplete[0].getTime() + + (timeStepsComplete[timeStepsComplete.length - 1].getTime() - + timeStepsComplete[0].getTime()) * + (target.scrollLeft / (dayContainerScrollWidth - viewWidth)) + ); + $shadeMap?.setDate(timeStep); + if (timeStep) currentDate = new SvelteDate(timeStep); }; @@ -649,14 +679,22 @@ // Clear isScrolling flag when scrolling ends isScrolling = false; - if (!desktop.current && !isDown) { + if (!desktop.current && !isDown && timeStepsComplete) { if ($loading) { centerDateButton($time); currentDate = new SvelteDate($time); } else { - let timeStep = findTimeStep(currentDate, timeSteps); + let timeStep = + timeStepsComplete[ + Math.round( + (timeStepsComplete.length * dayContainerScrollLeft) / + (dayContainerScrollWidth - viewWidth) + ) + ]; + timeStep = findTimeStep(timeStep, timeSteps); if (timeStep) currentDate = timeStep; onDateChange(currentDate); + isScrolling = true; centerDateButton(currentDate); } } @@ -669,7 +707,7 @@ const throttledScrollEvent = throttle((e: Event) => { onScrollEvent(e); - }, 25); + }, 0); const throttledScrollEndEvent = throttle(() => { onScrollEndEvent(); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 624b9f6e..f2668ed0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -18,6 +18,7 @@ export const DEFAULT_PREFERENCES = { hillshade: false, clipWater: false, showScale: true, + crepuscule: false, timeSelector: true }; diff --git a/src/lib/index.ts b/src/lib/index.ts index 740b7966..de82841e 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -26,6 +26,7 @@ import { opacity, preferences as p, resolution as r, + shadeMap as sM, tileSize as tS, url as u } from '$lib/stores/preferences'; @@ -602,6 +603,8 @@ const checkRasterLoaded = () => { } checked = 0; loading.set(false); + const shadeMap = get(sM); + if (shadeMap) shadeMap.setDate(get(time)); clearInterval(checkRasterSourceLoadedInterval); } }, 50); @@ -883,7 +886,7 @@ export const getInitialMetaData = async () => { const domain = get(selectedDomain); const uri = - domain && domain.value.startsWith('dwd_icon')&& !domain.value.endsWith('eps') + domain && domain.value.startsWith('dwd_icon') && !domain.value.endsWith('eps') ? `https://s3.servert.ch` : `https://map-tiles.open-meteo.com`; diff --git a/src/lib/stores/preferences.ts b/src/lib/stores/preferences.ts index a7930ec9..9206951d 100644 --- a/src/lib/stores/preferences.ts +++ b/src/lib/stores/preferences.ts @@ -24,6 +24,8 @@ import { } from './variables'; import { defaultVectorOptions, vectorOptions } from './vector'; +import type ShadeMap from 'mapbox-gl-shadow-simulator'; + export const defaultPreferences = DEFAULT_PREFERENCES; export interface Preferences { @@ -57,6 +59,8 @@ export const localStorageVersion: Persisted = persisted( export const helpOpen = writable(false); +export const shadeMap = writable(null); + export const resetStates = async () => { modelRunLocked.set(false); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 84a35c87..88736d63 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,10 +9,10 @@ omProtocol, updateCurrentBounds } from '@openmeteo/mapbox-layer'; + import ShadeMap from 'mapbox-gl-shadow-simulator'; import { type RequestParameters } from 'maplibre-gl'; import * as maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; - import { Protocol } from 'pmtiles'; import { toast } from 'svelte-sonner'; import { version } from '$app/environment'; @@ -26,6 +26,7 @@ resetStates, resolution, resolutionSet, + shadeMap, url } from '$lib/stores/preferences'; import { metaJson, modelRun, time } from '$lib/stores/time'; @@ -128,9 +129,31 @@ if (getInitialMetaDataPromise) await getInitialMetaDataPromise; addOmFileLayers(); + addHillshadeSources(); $map.addControl(new HillshadeButton()); + $shadeMap = new ShadeMap({ + date: $time, // display shadows for current date + color: '#01112f', // shade color + opacity: 0.7, // opacity of shade color + apiKey: '', // obtain from https://shademap.app/about/, + terrainSource: { + tileSize: 256, + maxZoom: 15, + + getSourceUrl: ({ x, y, z }) => { + return `https://tiles.mapterhorn.com/${z}/${x}/${y}.webp`; + }, + getElevation: ({ r, g, b, a }) => { + return r * 256 + g + b / 256 - 32768; + } + }, + debug: (msg) => { + console.log(new Date().toISOString(), msg); + } + }).addTo($map); + addPopup(); changeOMfileURL(); }); diff --git a/src/styles.css b/src/styles.css index 03f6ce7d..314bb447 100644 --- a/src/styles.css +++ b/src/styles.css @@ -316,4 +316,4 @@ body { /* IE and Edge */ scrollbar-width: none; /* Firefox */ -} \ No newline at end of file +}