diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 529c0d697..9bea8a7fc 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -81,7 +81,8 @@ They contain no application state. the rail cards, card blocks and timeline (`src/ui/rail/`) render through Vue's synchronous `render()` behind `createRailCards()`, `createRailCardBlocks()` and `createRailTimeline()`. Order: Scenes (done: `SceneOptions` and - `SceneShotList` over the `gev-scenes` store), Weather, Recent imagery, + `SceneShotList` over the `gev-scenes` store), Weather (done: the owner + renders `WeatherReadout` from the view it computes), Recent imagery, Street level, CCTV, Layers, Display, Context and Radio, the command dock, the phone shell, then Cockpit. Layers that write into the right rail directly gain a slot instead. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 73a3a95d6..3077af410 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -575,7 +575,8 @@ "src/ui/rail/RailCards.vue", "src/ui/rail/utcClock.js", "src/ui/rail/RailTimeline.vue", - "src/ui/rail/rangeValue.js" + "src/ui/rail/rangeValue.js", + "src/ui/weather/WeatherReadout.vue" ], "external": ["vue"] }, @@ -942,7 +943,8 @@ "src/ui/rail/RailCards.vue", "src/ui/rail/utcClock.js", "src/ui/rail/RailTimeline.vue", - "src/ui/rail/rangeValue.js" + "src/ui/rail/rangeValue.js", + "src/ui/weather/WeatherReadout.vue" ], "external": ["cesium", "vue", "pinia"] }, @@ -2904,7 +2906,8 @@ "src/ui/rail/RailCardBlocks.vue", "src/ui/rail/utcClock.js", "src/ui/rail/RailTimeline.vue", - "src/ui/rail/rangeValue.js" + "src/ui/rail/rangeValue.js", + "src/ui/weather/WeatherReadout.vue" ], "external": [ "hls.js", diff --git a/src/ui/weather/WeatherReadout.vue b/src/ui/weather/WeatherReadout.vue new file mode 100644 index 000000000..c7c341816 --- /dev/null +++ b/src/ui/weather/WeatherReadout.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/ui/weatherPanel.js b/src/ui/weatherPanel.js index e37911a7c..554731e54 100644 --- a/src/ui/weatherPanel.js +++ b/src/ui/weatherPanel.js @@ -1,6 +1,7 @@ +import { h, render as renderVue } from 'vue'; import { formatUtcDayMonth, t } from '../i18n/index.js'; -import { createRailCards } from './railCards.js'; -import { createRailTimeline, utcClock } from './railTimeline.js'; +import { utcClock } from './rail/utcClock.js'; +import WeatherReadout from './weather/WeatherReadout.vue'; // Remounting controls must not reopen a panel the user already collapsed. // Keyed by the page's Weather panel, which outlives remounted controls. @@ -70,28 +71,6 @@ export function createWeatherPanel({ } = {}) { const document = container?.ownerDocument; if (!document?.createElement) return null; - const root = document.createElement('section'); - root.className = 'weather-readout'; - root.hidden = true; - root.setAttribute('aria-label', t('Active weather')); - const timelineHost = document.createElement('div'); - timelineHost.className = 'weather-timeline-block'; - timelineHost.hidden = true; - const cardsHost = document.createElement('div'); - cardsHost.className = 'weather-cards'; - const observedGroup = document.createElement('section'); - observedGroup.className = 'weather-observed-group'; - observedGroup.setAttribute('aria-label', t('Observed history')); - const heading = document.createElement('h3'); - heading.className = 'panel-title'; - heading.textContent = t('Observed history'); - const scope = document.createElement('div'); - scope.className = 'weather-observed-scope'; - const observedCardsHost = document.createElement('div'); - observedCardsHost.className = 'weather-cards'; - observedGroup.append(heading, scope, timelineHost, observedCardsHost); - root.append(cardsHost, observedGroup); - container.appendChild(root); const panel = container.closest?.('#weather-panel'); const count = panel?.querySelector('#weather-panel-count'); let entries = []; @@ -102,32 +81,38 @@ export function createWeatherPanel({ let openId = openPanels.get(page) || null; // Open card as last shown; null until cards are on screen. let shownOpenId = null; - const timeline = createRailTimeline({ - container: timelineHost, - document, - sliderClassName: 'weather-timeline', - heading: false, - onCommit: (tick) => clock?.setTarget(tick), - onPreview: historyTime, - onStep: (direction) => clock?.step(direction), - onLatest: () => clock?.latest(), - onPlay: () => clock?.togglePlay(), - }); - const cardOptions = { - document, - cardClassName: 'weather-card', - badgeClassName: 'weather-coverage', + const handlers = { onParams: (id, params) => setLayerParams(id, params, { origin: 'user' }), onOpen: (id) => { openId = id; openPanels.set(page, id); render(); }, + transport: { + onCommit: (tick) => clock?.setTarget(tick), + onPreview: historyTime, + onStep: (direction) => clock?.step(direction), + onLatest: () => clock?.latest(), + onPlay: () => clock?.togglePlay(), + }, }; - const cards = createRailCards({ ...cardOptions, container: cardsHost }); - const observedCards = createRailCards({ - ...cardOptions, - container: observedCardsHost, + // The readout patches synchronously, so a newly opened card can be measured. + const paint = (view) => + renderVue(h(WeatherReadout, { view, ...handlers }), container); + paint({ + hidden: true, + observed: false, + scope: '', + timeline: { + ticks: [], + index: 0, + mode: 'latest', + playing: false, + readout: '', + disabled: true, + }, + cards: [], + observedCards: [], }); const render = () => { if (destroyed) return; @@ -141,14 +126,11 @@ export function createWeatherPanel({ .sort((a, b) => ORDER.indexOf(a.id) - ORDER.indexOf(b.id)); const observed = active.filter(({ id }) => OBSERVED.has(id)); const showTimeline = observed.length > 0; - set(observedGroup, 'hidden', !showTimeline); const names = { 'weather-radar': t('Rain radar'), 'weather-satellite': t('Satellite clouds'), 'weather-lightning': t('Lightning density'), }; - set(scope, 'textContent', observed.map(({ id }) => names[id]).join(' · ')); - set(timelineHost, 'hidden', !showTimeline); const index = state.mode === 'latest' ? state.timeline.length - 1 @@ -158,7 +140,7 @@ export function createWeatherPanel({ (tick) => Date.parse(tick) <= Date.parse(state.target), ), ); - timeline.update({ + const timeline = { ticks: state.timeline, index, mode: state.mode, @@ -168,7 +150,7 @@ export function createWeatherPanel({ state.mode === 'latest' ? t('LATEST · newest per product') : historyTime(state.target), - }); + }; const models = active.map((entry) => { const { id, icon, summary, legend = [], list } = entry; // Discrete classes (storm geometry, AQHI bands) list swatches, not a ramp. @@ -248,18 +230,23 @@ export function createWeatherPanel({ blocks, }; }); - cards.update(models.filter(({ id }) => !OBSERVED.has(id))); - observedCards.update(models.filter(({ id }) => OBSERVED.has(id))); + const hidden = active.length === 0; + paint({ + hidden, + observed: showTimeline, + scope: observed.map(({ id }) => names[id]).join(' · '), + timeline, + cards: models.filter(({ id }) => !OBSERVED.has(id)), + observedCards: models.filter(({ id }) => OBSERVED.has(id)), + }); // Only a change of open card moves the scroll position; refreshes leave it. if (shownOpenId !== null && openId !== shownOpenId) { - const open = [...cardsHost.children, ...observedCardsHost.children].find( - (node) => node.dataset?.cardId === openId, - ); + const open = [ + ...container.querySelectorAll('.weather-cards > [data-card-id]'), + ].find((node) => node.dataset.cardId === openId); revealCard(container, open); } shownOpenId = active.length ? openId : null; - const hidden = active.length === 0; - set(root, 'hidden', hidden); if (count) set(count, 'textContent', String(active.length)); if (panel) set(panel, 'hidden', hidden); if (!hidden && panel && !appearedPanels.has(panel)) { @@ -305,10 +292,7 @@ export function createWeatherPanel({ destroy() { destroyed = true; unsubscribe?.(); - timeline.destroy(); - cards.destroy(); - observedCards.destroy(); - root.remove(); + renderVue(null, container); if (panel) set(panel, 'hidden', true); if (count) set(count, 'textContent', '0'); },