feat(scenes): render the data-pack attribution cards from a store

The data-pack presentations keep the Cesium resources, object URLs and
pick targets and present each pack's attribution card, with its media
player, to the gev-data-packs store; the DataPacks view teleports one
panel per set of presentations into the viewer's container with the same
markup and inline styles, and stops a removed card's player as it goes.
This commit is contained in:
Sameh Khamis committed 2026-10-31 20:22:22 -07:00
1 parent 59e3aaa271
commit e0abf74308
8 files changed
+290 -69

No files matched your search

+59 -65
View File
@@ -1,55 +1,56 @@
import * as Cesium from 'cesium';
import { renderPackGeometry } from './geometry.js';
import { t } from '../../i18n/index.js';
import { useDataPacksStore } from '../../ui/stores/dataPacks.js';
import { applicationPinia } from '../../ui/vueRuntime.js';
function caption(viewer, pack) {
const card = document.createElement('section');
card.dataset.directorPack = pack.id;
Object.assign(card.style, {
color: '#e3faff',
background: '#08141eed',
border: '1px solid #2491a8',
padding: '8px',
margin: '4px',
maxWidth: '260px',
font: '12px sans-serif',
});
const title = document.createElement('div');
title.textContent = `${pack.id} · ${pack.attribution.text} · ${pack.attribution.license}`;
card.append(title);
if (pack.attribution.url) {
const link = document.createElement('a');
link.textContent = t('Source');
link.href = pack.attribution.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.color = '#6eeaff';
card.append(link);
}
return card;
}
const xyz = (p) => Cesium.Cartesian3.fromDegrees(...p);
/** Create rendering adapters; data acquisition and authored placement stay independent. */
export function createPackPresentations(viewer, targets = new Map()) {
let panel;
// Key every panel and card on the page, so a new one never reuses an old
// one's DOM.
let panelKeys = 0,
cardKeys = 0;
/**
* Create rendering adapters; data acquisition and authored placement stay
* independent. Each pack's attribution card is presented to the data packs
* store, which its view renders inside the viewer's container.
*/
export function createPackPresentations(
viewer,
targets = new Map(),
{ store = useDataPacksStore(applicationPinia()) } = {},
) {
const owner = {};
let key,
cards = [];
function present() {
store.publish(
owner,
cards.length ? { key, container: viewer.container, cards } : null,
);
}
function addCard(pack) {
if (!panel) {
panel = document.createElement('div');
panel.dataset.directorPacks = '';
Object.assign(panel.style, {
position: 'absolute',
right: '10px',
bottom: '70px',
maxHeight: '40vh',
overflowY: 'auto',
zIndex: '30',
});
viewer.container.append(panel);
}
const card = caption(viewer, pack);
panel.append(card);
return card;
if (!cards.length) key = ++panelKeys;
const card = {
key: ++cardKeys,
id: pack.id,
title: `${pack.id} · ${pack.attribution.text} · ${pack.attribution.license}`,
url: pack.attribution.url || null,
media: null,
};
cards = [...cards, card];
present();
return card.key;
}
function updateCard(key, patch) {
cards = cards.map((card) =>
card.key === key ? { ...card, ...patch } : card,
);
present();
}
function removeCard(key) {
cards = cards.filter((card) => card.key !== key);
present();
}
function resource(pack) {
const entities = [],
@@ -78,7 +79,12 @@ export function createPackPresentations(viewer, targets = new Map()) {
viewer.scene.requestRender();
},
card() {
return (card ||= addCard(pack));
card ||= addCard(pack);
},
/** @param {{kind: 'video'|'audio', src: string}} media */
media(media) {
card ||= addCard(pack);
updateCard(card, { media });
},
url(blob) {
const url = URL.createObjectURL(blob);
@@ -89,16 +95,8 @@ export function createPackPresentations(viewer, targets = new Map()) {
if (destroyed) return;
destroyed = true;
for (const key of featureKeys.splice(0)) targets.delete(key);
for (const media of card?.querySelectorAll('video,audio') || []) {
media.pause();
media.removeAttribute('src');
media.load();
}
card?.remove();
if (panel && !panel.childElementCount) {
panel.remove();
panel = null;
}
// The view stops a removed card's player as it goes.
if (card) removeCard(card);
for (const entity of entities.splice(0)) viewer.entities.remove(entity);
for (const primitive of primitives.splice(0))
viewer.scene.primitives.remove(primitive);
@@ -212,14 +210,10 @@ export function createPackPresentations(viewer, targets = new Map()) {
pixelOffset: new Cesium.Cartesian2(0, -24),
},
});
const media = document.createElement(
asset.mimeType.startsWith('video/') ? 'video' : 'audio',
);
media.controls = true;
media.preload = 'none';
media.style.width = '240px';
media.src = handle.url(new Blob([asset.bytes], { type: asset.mimeType }));
handle.card().append(media);
handle.media({
kind: asset.mimeType.startsWith('video/') ? 'video' : 'audio',
src: handle.url(new Blob([asset.bytes], { type: asset.mimeType })),
});
}),
};
}