|
84 | 84 | // 3 h step so changes line up with both wind + wave file cadences. |
85 | 85 | let weatherForecastHour = $state(0); |
86 | 86 | let weatherRunTime = $state<string | null>(null); |
87 | | - let weatherLoading = $state(false); |
| 87 | + // Per-layer fetch state. Default true because both setupWeatherLayer |
| 88 | + // calls fire at mount; flipped false in each .finally below. Reused |
| 89 | + // for refetches (model swap, forecast-hour scrub) so the spinner in |
| 90 | + // the forecast bar covers both initial load and subsequent fetches. |
| 91 | + let windLoading = $state(true); |
| 92 | + let waveLoading = $state(true); |
| 93 | + let weatherLoading = $derived(windLoading || waveLoading); |
88 | 94 | // First forecast hour ≥ "now", computed from the GFS run time. Used |
89 | 95 | // as the slider minimum so the user can't scrub back into already- |
90 | 96 | // expired analysis hours. |
|
3036 | 3042 | }) |
3037 | 3043 | .catch((err) => { |
3038 | 3044 | console.warn("wind layer disabled:", err); |
| 3045 | + }) |
| 3046 | + .finally(() => { |
| 3047 | + windLoading = false; |
3039 | 3048 | }); |
3040 | 3049 | // Wave overlay: ol-wind particle animation driven by Thgt+Tdir |
3041 | 3050 | // from PacIOOS WaveWatch III, fetched server-side via OPeNDAP |
|
3076 | 3085 | }) |
3077 | 3086 | .catch((err) => { |
3078 | 3087 | console.warn("wave layer disabled:", err); |
| 3088 | + }) |
| 3089 | + .finally(() => { |
| 3090 | + waveLoading = false; |
3079 | 3091 | }); |
3080 | 3092 | // Populate the model picker. Failures are non-fatal — the picker |
3081 | 3093 | // just stays at the bundled defaults (GFS + PacIOOS) if the |
|
4860 | 4872 | value={windModel} |
4861 | 4873 | onchange={async (e) => { |
4862 | 4874 | const next = (e.currentTarget as HTMLSelectElement).value; |
4863 | | - weatherLoading = true; |
| 4875 | + windLoading = true; |
4864 | 4876 | setWeatherError(null); |
4865 | 4877 | try { |
4866 | 4878 | const err = await windHandle?.setModel(next); |
|
4884 | 4896 | mapGlobal.map?.render(); |
4885 | 4897 | } |
4886 | 4898 | } finally { |
4887 | | - weatherLoading = false; |
| 4899 | + windLoading = false; |
4888 | 4900 | } |
4889 | 4901 | }} |
4890 | 4902 | > |
|
4904 | 4916 | value={waveModel} |
4905 | 4917 | onchange={async (e) => { |
4906 | 4918 | const next = (e.currentTarget as HTMLSelectElement).value; |
4907 | | - weatherLoading = true; |
| 4919 | + waveLoading = true; |
4908 | 4920 | setWeatherError(null); |
4909 | 4921 | try { |
4910 | 4922 | const err = await swapWaveModel(next); |
|
4916 | 4928 | (e.currentTarget as HTMLSelectElement).value = waveModel; |
4917 | 4929 | } |
4918 | 4930 | } finally { |
4919 | | - weatherLoading = false; |
| 4931 | + waveLoading = false; |
4920 | 4932 | } |
4921 | 4933 | }} |
4922 | 4934 | > |
|
4940 | 4952 | {:else} |
4941 | 4953 | +{weatherForecastHour}h |
4942 | 4954 | {/if} |
4943 | | - {#if weatherLoading}…{/if} |
| 4955 | + {#if weatherLoading} |
| 4956 | + <span class="wind-forecast-bar-spinner" aria-label="loading" title="loading"></span> |
| 4957 | + {/if} |
4944 | 4958 | </label> |
4945 | 4959 | <div class="wind-forecast-bar-slider-wrap"> |
4946 | 4960 | {#each dayMarkers as m (m.pct)} |
|
4971 | 4985 | // re-render at the stale old value and snap the thumb back |
4972 | 4986 | // to the slider's min. |
4973 | 4987 | weatherForecastHour = v; |
4974 | | - weatherLoading = true; |
| 4988 | + if (windHandle) windLoading = true; |
| 4989 | + if (waveHandle) waveLoading = true; |
4975 | 4990 | // Hide both data layers while we refetch so the user doesn't |
4976 | 4991 | // see the previous forecast hour's pixels under the new |
4977 | 4992 | // forecast-hour label — purely visual, the OL layers stay |
|
5001 | 5016 | } |
5002 | 5017 | mapGlobal.map?.render(); |
5003 | 5018 | } finally { |
5004 | | - weatherLoading = false; |
| 5019 | + windLoading = false; |
| 5020 | + waveLoading = false; |
5005 | 5021 | windLayer?.setVisible?.(true); |
5006 | 5022 | waveLayer?.setVisible?.(true); |
5007 | 5023 | } |
|
6464 | 6480 | min-width: 70px; |
6465 | 6481 | text-align: right; |
6466 | 6482 | } |
| 6483 | + .wind-forecast-bar-spinner { |
| 6484 | + display: inline-block; |
| 6485 | + width: 12px; |
| 6486 | + height: 12px; |
| 6487 | + margin-left: 6px; |
| 6488 | + vertical-align: -2px; |
| 6489 | + border: 2px solid rgba(0, 0, 0, 0.18); |
| 6490 | + border-top-color: #0a66c2; |
| 6491 | + border-radius: 50%; |
| 6492 | + animation: wind-forecast-bar-spin 0.7s linear infinite; |
| 6493 | + } |
| 6494 | + @keyframes wind-forecast-bar-spin { |
| 6495 | + to { |
| 6496 | + transform: rotate(360deg); |
| 6497 | + } |
| 6498 | + } |
6467 | 6499 | .wind-forecast-bar input[type="range"] { |
6468 | 6500 | width: 240px; |
6469 | 6501 | display: block; |
|
0 commit comments