mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(ui): render the Weather readout as one component
createWeatherPanel() computes the readout it shows (forecast and event cards, the observed group's scope, the history timeline state and observed cards) and renders WeatherReadout synchronously into the panel body, so a newly opened card is still revealed after measuring. The hand-built hosts and the separate card and timeline builders are gone; markup, open-card choice, first appearance and teardown are unchanged.
This commit is contained in:
1 parent
ce6040bde1
commit
a318376203
4 files changed
+103
-63
No files matched your search
@@ -81,7 +81,8 @@ They contain no application state.
|
|||||||
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
|
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
|
||||||
Vue's synchronous `render()` behind `createRailCards()`,
|
Vue's synchronous `render()` behind `createRailCards()`,
|
||||||
`createRailCardBlocks()` and `createRailTimeline()`. Order: Scenes (done: `SceneOptions` and
|
`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,
|
Street level, CCTV, Layers, Display, Context and Radio, the command dock,
|
||||||
the phone shell, then Cockpit. Layers that write into the right rail
|
the phone shell, then Cockpit. Layers that write into the right rail
|
||||||
directly gain a slot instead.
|
directly gain a slot instead.
|
||||||
|
|||||||
@@ -575,7 +575,8 @@
|
|||||||
"src/ui/rail/RailCards.vue",
|
"src/ui/rail/RailCards.vue",
|
||||||
"src/ui/rail/utcClock.js",
|
"src/ui/rail/utcClock.js",
|
||||||
"src/ui/rail/RailTimeline.vue",
|
"src/ui/rail/RailTimeline.vue",
|
||||||
"src/ui/rail/rangeValue.js"
|
"src/ui/rail/rangeValue.js",
|
||||||
|
"src/ui/weather/WeatherReadout.vue"
|
||||||
],
|
],
|
||||||
"external": ["vue"]
|
"external": ["vue"]
|
||||||
},
|
},
|
||||||
@@ -942,7 +943,8 @@
|
|||||||
"src/ui/rail/RailCards.vue",
|
"src/ui/rail/RailCards.vue",
|
||||||
"src/ui/rail/utcClock.js",
|
"src/ui/rail/utcClock.js",
|
||||||
"src/ui/rail/RailTimeline.vue",
|
"src/ui/rail/RailTimeline.vue",
|
||||||
"src/ui/rail/rangeValue.js"
|
"src/ui/rail/rangeValue.js",
|
||||||
|
"src/ui/weather/WeatherReadout.vue"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "vue", "pinia"]
|
"external": ["cesium", "vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -2904,7 +2906,8 @@
|
|||||||
"src/ui/rail/RailCardBlocks.vue",
|
"src/ui/rail/RailCardBlocks.vue",
|
||||||
"src/ui/rail/utcClock.js",
|
"src/ui/rail/utcClock.js",
|
||||||
"src/ui/rail/RailTimeline.vue",
|
"src/ui/rail/RailTimeline.vue",
|
||||||
"src/ui/rail/rangeValue.js"
|
"src/ui/rail/rangeValue.js",
|
||||||
|
"src/ui/weather/WeatherReadout.vue"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup>
|
||||||
|
import { t } from '../../i18n/index.js';
|
||||||
|
import RailCards from '../rail/RailCards.vue';
|
||||||
|
import RailTimeline from '../rail/RailTimeline.vue';
|
||||||
|
|
||||||
|
// Active weather: forecast and event cards, then the observed products with
|
||||||
|
// their shared history timeline. The owner computes every value shown.
|
||||||
|
defineProps({
|
||||||
|
view: { type: Object, required: true },
|
||||||
|
onParams: { type: Function, default: () => {} },
|
||||||
|
onOpen: { type: Function, default: () => {} },
|
||||||
|
transport: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="weather-readout"
|
||||||
|
:hidden="view.hidden"
|
||||||
|
:aria-label="t('Active weather')"
|
||||||
|
><div class="weather-cards"
|
||||||
|
><RailCards
|
||||||
|
:cards="view.cards"
|
||||||
|
card-class-name="weather-card"
|
||||||
|
badge-class-name="weather-coverage"
|
||||||
|
:on-params="onParams"
|
||||||
|
:on-open="onOpen" /></div
|
||||||
|
><section
|
||||||
|
class="weather-observed-group"
|
||||||
|
:hidden="!view.observed"
|
||||||
|
:aria-label="t('Observed history')"
|
||||||
|
><h3 class="panel-title">{{ t('Observed history') }}</h3
|
||||||
|
><div class="weather-observed-scope">{{ view.scope }}</div
|
||||||
|
><div class="weather-timeline-block" :hidden="!view.observed"
|
||||||
|
><RailTimeline
|
||||||
|
:state="view.timeline"
|
||||||
|
slider-class-name="weather-timeline"
|
||||||
|
:heading="false"
|
||||||
|
:on-commit="transport.onCommit"
|
||||||
|
:on-preview="transport.onPreview"
|
||||||
|
:on-step="transport.onStep"
|
||||||
|
:on-latest="transport.onLatest"
|
||||||
|
:on-play="transport.onPlay" /></div
|
||||||
|
><div class="weather-cards"
|
||||||
|
><RailCards
|
||||||
|
:cards="view.observedCards"
|
||||||
|
card-class-name="weather-card"
|
||||||
|
badge-class-name="weather-coverage"
|
||||||
|
:on-params="onParams"
|
||||||
|
:on-open="onOpen" /></div></section
|
||||||
|
></section>
|
||||||
|
</template>
|
||||||
+43
-59
@@ -1,6 +1,7 @@
|
|||||||
|
import { h, render as renderVue } from 'vue';
|
||||||
import { formatUtcDayMonth, t } from '../i18n/index.js';
|
import { formatUtcDayMonth, t } from '../i18n/index.js';
|
||||||
import { createRailCards } from './railCards.js';
|
import { utcClock } from './rail/utcClock.js';
|
||||||
import { createRailTimeline, utcClock } from './railTimeline.js';
|
import WeatherReadout from './weather/WeatherReadout.vue';
|
||||||
|
|
||||||
// Remounting controls must not reopen a panel the user already collapsed.
|
// Remounting controls must not reopen a panel the user already collapsed.
|
||||||
// Keyed by the page's Weather panel, which outlives remounted controls.
|
// Keyed by the page's Weather panel, which outlives remounted controls.
|
||||||
@@ -70,28 +71,6 @@ export function createWeatherPanel({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
const document = container?.ownerDocument;
|
const document = container?.ownerDocument;
|
||||||
if (!document?.createElement) return null;
|
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 panel = container.closest?.('#weather-panel');
|
||||||
const count = panel?.querySelector('#weather-panel-count');
|
const count = panel?.querySelector('#weather-panel-count');
|
||||||
let entries = [];
|
let entries = [];
|
||||||
@@ -102,32 +81,38 @@ export function createWeatherPanel({
|
|||||||
let openId = openPanels.get(page) || null;
|
let openId = openPanels.get(page) || null;
|
||||||
// Open card as last shown; null until cards are on screen.
|
// Open card as last shown; null until cards are on screen.
|
||||||
let shownOpenId = null;
|
let shownOpenId = null;
|
||||||
const timeline = createRailTimeline({
|
const handlers = {
|
||||||
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',
|
|
||||||
onParams: (id, params) => setLayerParams(id, params, { origin: 'user' }),
|
onParams: (id, params) => setLayerParams(id, params, { origin: 'user' }),
|
||||||
onOpen: (id) => {
|
onOpen: (id) => {
|
||||||
openId = id;
|
openId = id;
|
||||||
openPanels.set(page, id);
|
openPanels.set(page, id);
|
||||||
render();
|
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 });
|
// The readout patches synchronously, so a newly opened card can be measured.
|
||||||
const observedCards = createRailCards({
|
const paint = (view) =>
|
||||||
...cardOptions,
|
renderVue(h(WeatherReadout, { view, ...handlers }), container);
|
||||||
container: observedCardsHost,
|
paint({
|
||||||
|
hidden: true,
|
||||||
|
observed: false,
|
||||||
|
scope: '',
|
||||||
|
timeline: {
|
||||||
|
ticks: [],
|
||||||
|
index: 0,
|
||||||
|
mode: 'latest',
|
||||||
|
playing: false,
|
||||||
|
readout: '',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
cards: [],
|
||||||
|
observedCards: [],
|
||||||
});
|
});
|
||||||
const render = () => {
|
const render = () => {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
@@ -141,14 +126,11 @@ export function createWeatherPanel({
|
|||||||
.sort((a, b) => ORDER.indexOf(a.id) - ORDER.indexOf(b.id));
|
.sort((a, b) => ORDER.indexOf(a.id) - ORDER.indexOf(b.id));
|
||||||
const observed = active.filter(({ id }) => OBSERVED.has(id));
|
const observed = active.filter(({ id }) => OBSERVED.has(id));
|
||||||
const showTimeline = observed.length > 0;
|
const showTimeline = observed.length > 0;
|
||||||
set(observedGroup, 'hidden', !showTimeline);
|
|
||||||
const names = {
|
const names = {
|
||||||
'weather-radar': t('Rain radar'),
|
'weather-radar': t('Rain radar'),
|
||||||
'weather-satellite': t('Satellite clouds'),
|
'weather-satellite': t('Satellite clouds'),
|
||||||
'weather-lightning': t('Lightning density'),
|
'weather-lightning': t('Lightning density'),
|
||||||
};
|
};
|
||||||
set(scope, 'textContent', observed.map(({ id }) => names[id]).join(' · '));
|
|
||||||
set(timelineHost, 'hidden', !showTimeline);
|
|
||||||
const index =
|
const index =
|
||||||
state.mode === 'latest'
|
state.mode === 'latest'
|
||||||
? state.timeline.length - 1
|
? state.timeline.length - 1
|
||||||
@@ -158,7 +140,7 @@ export function createWeatherPanel({
|
|||||||
(tick) => Date.parse(tick) <= Date.parse(state.target),
|
(tick) => Date.parse(tick) <= Date.parse(state.target),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
timeline.update({
|
const timeline = {
|
||||||
ticks: state.timeline,
|
ticks: state.timeline,
|
||||||
index,
|
index,
|
||||||
mode: state.mode,
|
mode: state.mode,
|
||||||
@@ -168,7 +150,7 @@ export function createWeatherPanel({
|
|||||||
state.mode === 'latest'
|
state.mode === 'latest'
|
||||||
? t('LATEST · newest per product')
|
? t('LATEST · newest per product')
|
||||||
: historyTime(state.target),
|
: historyTime(state.target),
|
||||||
});
|
};
|
||||||
const models = active.map((entry) => {
|
const models = active.map((entry) => {
|
||||||
const { id, icon, summary, legend = [], list } = entry;
|
const { id, icon, summary, legend = [], list } = entry;
|
||||||
// Discrete classes (storm geometry, AQHI bands) list swatches, not a ramp.
|
// Discrete classes (storm geometry, AQHI bands) list swatches, not a ramp.
|
||||||
@@ -248,18 +230,23 @@ export function createWeatherPanel({
|
|||||||
blocks,
|
blocks,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
cards.update(models.filter(({ id }) => !OBSERVED.has(id)));
|
const hidden = active.length === 0;
|
||||||
observedCards.update(models.filter(({ id }) => OBSERVED.has(id)));
|
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.
|
// Only a change of open card moves the scroll position; refreshes leave it.
|
||||||
if (shownOpenId !== null && openId !== shownOpenId) {
|
if (shownOpenId !== null && openId !== shownOpenId) {
|
||||||
const open = [...cardsHost.children, ...observedCardsHost.children].find(
|
const open = [
|
||||||
(node) => node.dataset?.cardId === openId,
|
...container.querySelectorAll('.weather-cards > [data-card-id]'),
|
||||||
);
|
].find((node) => node.dataset.cardId === openId);
|
||||||
revealCard(container, open);
|
revealCard(container, open);
|
||||||
}
|
}
|
||||||
shownOpenId = active.length ? openId : null;
|
shownOpenId = active.length ? openId : null;
|
||||||
const hidden = active.length === 0;
|
|
||||||
set(root, 'hidden', hidden);
|
|
||||||
if (count) set(count, 'textContent', String(active.length));
|
if (count) set(count, 'textContent', String(active.length));
|
||||||
if (panel) set(panel, 'hidden', hidden);
|
if (panel) set(panel, 'hidden', hidden);
|
||||||
if (!hidden && panel && !appearedPanels.has(panel)) {
|
if (!hidden && panel && !appearedPanels.has(panel)) {
|
||||||
@@ -305,10 +292,7 @@ export function createWeatherPanel({
|
|||||||
destroy() {
|
destroy() {
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
unsubscribe?.();
|
unsubscribe?.();
|
||||||
timeline.destroy();
|
renderVue(null, container);
|
||||||
cards.destroy();
|
|
||||||
observedCards.destroy();
|
|
||||||
root.remove();
|
|
||||||
if (panel) set(panel, 'hidden', true);
|
if (panel) set(panel, 'hidden', true);
|
||||||
if (count) set(count, 'textContent', '0');
|
if (count) set(count, 'textContent', '0');
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user