Skip to content

Commit 14bd560

Browse files
committed
WIP: attempt enriched Cesium sample display
1 parent 939847e commit 14bd560

1 file changed

Lines changed: 182 additions & 14 deletions

File tree

tutorials/parquet_cesium.qmd

Lines changed: 182 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,34 @@ This page demonstrates how geospatial data can be dynamically accessed from a re
2222
#cesiumContainer {
2323
aspect-ratio: 1/1;
2424
}
25+
#sampleDetails {
26+
margin-top: 1.5rem;
27+
}
28+
#sampleDetails .sample-grid {
29+
display: grid;
30+
gap: 1rem;
31+
}
32+
@media (min-width: 768px) {
33+
#sampleDetails .sample-grid {
34+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
35+
}
36+
}
37+
#sampleDetails .sample-card {
38+
border: 1px solid #d9d9d9;
39+
border-radius: 0.5rem;
40+
padding: 0.75rem;
41+
background: #fafafa;
42+
}
43+
#sampleDetails .sample-card h3 {
44+
margin-top: 0;
45+
font-size: 1.05rem;
46+
}
47+
#sampleDetails .sample-card img {
48+
max-width: 140px;
49+
border-radius: 0.25rem;
50+
display: block;
51+
margin-top: 0.5rem;
52+
}
2553
</style>
2654

2755
```{ojs}
@@ -174,7 +202,13 @@ class CView {
174202
this.selectHandler.setInputAction((e) => {
175203
const selectPoint = this.viewer.scene.pick(e.position);
176204
if (Cesium.defined(selectPoint) && selectPoint.hasOwnProperty("primitive")) {
177-
mutable clickedPointId = selectPoint.id;
205+
console.log("Clicked point ID:", selectPoint.id);
206+
// Store the clicked ID in the viewer instance for now
207+
this.clickedId = selectPoint.id;
208+
// Dispatch a custom event that can be picked up by Observable
209+
document.dispatchEvent(new CustomEvent('pointSelected', {
210+
detail: { pointId: selectPoint.id }
211+
}));
178212
}
179213
},Cesium.ScreenSpaceEventType.LEFT_CLICK);
180214
@@ -196,16 +230,114 @@ async function getGeoRecord(pid) {
196230
return result;
197231
}
198232
199-
async function locationUsedBy(rowid){
233+
async function samplesAtLocation(rowid) {
200234
if (rowid === undefined || rowid === null) {
201235
return [];
202236
}
203-
const q = `select pid, otype from nodes where row_id in (select nodes.s from nodes where list_contains(nodes.o, ?));`;
204-
return db.query(q, [rowid]);
237+
const query = `
238+
WITH edges AS (
239+
SELECT s, p, unnest(o) AS o1
240+
FROM nodes
241+
WHERE otype = '_edge_'
242+
), events AS (
243+
SELECT s AS event_row_id
244+
FROM edges
245+
WHERE p = 'sample_location' AND o1 = ?
246+
), sample_links AS (
247+
SELECT s AS sample_row_id, o1 AS event_row_id
248+
FROM edges
249+
WHERE p = 'produced_by' AND o1 IN (SELECT event_row_id FROM events)
250+
), sample_nodes AS (
251+
SELECT row_id, pid, label, description, thumbnail_url, alternate_identifiers
252+
FROM nodes
253+
WHERE row_id IN (SELECT sample_row_id FROM sample_links)
254+
), event_nodes AS (
255+
SELECT row_id, label, project
256+
FROM nodes
257+
WHERE row_id IN (SELECT event_row_id FROM events)
258+
), concept_edges AS (
259+
SELECT s, p, o1
260+
FROM edges
261+
WHERE s IN (SELECT row_id FROM sample_nodes)
262+
AND p IN ('has_sample_object_type','has_material_category','has_context_category','keywords')
263+
), concept_labels AS (
264+
SELECT row_id, label
265+
FROM nodes
266+
WHERE row_id IN (SELECT o1 FROM concept_edges)
267+
), keyword_text AS (
268+
SELECT ce.s, string_agg(DISTINCT cl.label, ', ') AS keywords
269+
FROM concept_edges ce
270+
JOIN concept_labels cl ON ce.o1 = cl.row_id
271+
WHERE ce.p = 'keywords'
272+
GROUP BY ce.s
273+
), sampling_sites AS (
274+
SELECT s AS event_row_id, o1 AS site_row_id
275+
FROM edges
276+
WHERE p = 'sampling_site' AND s IN (SELECT event_row_id FROM events)
277+
), site_nodes AS (
278+
SELECT row_id, label
279+
FROM nodes
280+
WHERE row_id IN (SELECT site_row_id FROM sampling_sites)
281+
)
282+
SELECT
283+
sn.pid,
284+
sn.label,
285+
sn.description,
286+
sn.thumbnail_url,
287+
MAX(CASE WHEN ce.p = 'has_sample_object_type' THEN cl.label END) AS sample_object_type,
288+
MAX(CASE WHEN ce.p = 'has_material_category' THEN cl.label END) AS material_category,
289+
MAX(CASE WHEN ce.p = 'has_context_category' THEN cl.label END) AS context_category,
290+
kt.keywords,
291+
en.project,
292+
en.label AS event_label,
293+
snl.label AS site_label
294+
FROM sample_nodes sn
295+
LEFT JOIN sample_links sl ON sn.row_id = sl.sample_row_id
296+
LEFT JOIN event_nodes en ON sl.event_row_id = en.row_id
297+
LEFT JOIN concept_edges ce ON sn.row_id = ce.s
298+
LEFT JOIN concept_labels cl ON ce.o1 = cl.row_id
299+
LEFT JOIN keyword_text kt ON sn.row_id = kt.s
300+
LEFT JOIN sampling_sites ss ON sl.event_row_id = ss.event_row_id
301+
LEFT JOIN site_nodes snl ON ss.site_row_id = snl.row_id
302+
GROUP BY sn.pid, sn.label, sn.description, sn.thumbnail_url, kt.keywords, en.project, en.label, snl.label
303+
ORDER BY sn.label
304+
LIMIT 6;
305+
`;
306+
const result = await db.query(query, [rowid]);
307+
const rows = result?.toArray ? result.toArray() : result;
308+
return Array.isArray(rows) ? rows : [];
205309
}
206310
207-
mutable clickedPointId = "unset";
208-
selectedGeoRecord = await getGeoRecord(clickedPointId);
311+
// Use a viewof pattern to create a reactive clickedPointId
312+
viewof clickedPointId = {
313+
const input = html`<input type="hidden" value="unset">`;
314+
315+
// Listen for point selection events and update the input value
316+
document.addEventListener('pointSelected', (event) => {
317+
input.value = event.detail.pointId;
318+
input.dispatchEvent(new Event('input'));
319+
});
320+
321+
return input;
322+
}
323+
324+
// Access the current value
325+
clickedPointId;
326+
327+
selectedGeoRecord = {
328+
// This will re-execute whenever clickedPointId changes
329+
const result = await getGeoRecord(clickedPointId);
330+
return result;
331+
}
332+
333+
selectedSamples = {
334+
// This will re-execute whenever selectedGeoRecord changes
335+
if (selectedGeoRecord?.row_id) {
336+
const samples = await samplesAtLocation(selectedGeoRecord.row_id);
337+
return samples;
338+
}
339+
return [];
340+
}
209341
210342
md`Retrieved ${pointdata.length} locations from ${parquet_path}.`;
211343
```
@@ -238,14 +370,50 @@ viewof pointdata = {
238370

239371
:::
240372

241-
The click point ID is "${clickedPointId}".
242-
243373
```{ojs}
244374
//| echo: false
245-
md`\`\`\`
246-
${JSON.stringify(selectedGeoRecord, null, 2)}
247-
\`\`\`
248-
`
375+
// Enhanced UI with sample information
376+
html`<section id="sampleDetails">
377+
<h2>Selected Location</h2>
378+
${clickedPointId === "unset" ?
379+
html`<p>Click a point on the map to view nearby sample records.</p>` :
380+
html`${(() => {
381+
const locationPid = selectedGeoRecord?.pid || clickedPointId;
382+
const resolver = locationPid?.startsWith('http') ? locationPid : `https://n2t.net/${encodeURIComponent(locationPid)}`;
383+
const lat = selectedGeoRecord?.latitude != null ? selectedGeoRecord.latitude.toFixed(4) : 'N/A';
384+
const lon = selectedGeoRecord?.longitude != null ? selectedGeoRecord.longitude.toFixed(4) : 'N/A';
385+
const samples = Array.isArray(selectedSamples) ? selectedSamples : [];
386+
return html`<div>
387+
<p><strong>Point PID:</strong> <a href="${resolver}" target="_blank" rel="noopener">${locationPid}</a></p>
388+
<p><strong>Coordinates:</strong> ${lat}, ${lon}</p>
389+
<p><strong>Sample records found:</strong> ${samples.length}</p>
390+
${samples.length
391+
? html`<div class="sample-grid">
392+
${samples.map(sample => {
393+
const displayLabel = sample.label || sample.pid;
394+
const sampleLink = sample.pid?.startsWith('http') ? sample.pid : `https://n2t.net/${encodeURIComponent(sample.pid)}`;
395+
const typeParts = [sample.sample_object_type, sample.material_category].filter(Boolean);
396+
const collectionParts = [sample.project, sample.site_label, sample.event_label].filter(Boolean);
397+
const context = sample.context_category;
398+
const keywords = typeof sample.keywords === 'string'
399+
? sample.keywords.split(', ').filter(Boolean)
400+
: Array.isArray(sample.keywords)
401+
? sample.keywords.filter(Boolean)
402+
: [];
403+
return html`<article class="sample-card">
404+
<h3><a href="${sampleLink}" target="_blank" rel="noopener">${displayLabel}</a></h3>
405+
${sample.description ? html`<p>${sample.description}</p>` : null}
406+
${typeParts.length ? html`<p><strong>Type</strong>: ${typeParts.join(' · ')}</p>` : null}
407+
${context ? html`<p><strong>Context</strong>: ${context}</p>` : null}
408+
${collectionParts.length ? html`<p><strong>Collection</strong>: ${collectionParts.join(' · ')}</p>` : null}
409+
${keywords.length ? html`<p><strong>Keywords</strong>: ${keywords.join(', ')}</p>` : null}
410+
${sample.thumbnail_url ? html`<img src="${sample.thumbnail_url}" alt="Thumbnail for ${displayLabel}" loading="lazy">` : null}
411+
</article>`;
412+
})}
413+
</div>`
414+
: html`<p><em>No sample records were linked to this location.</em></p>`}
415+
</div>`;
416+
})()}`
417+
}
418+
</section>`
249419
```
250-
251-

0 commit comments

Comments
 (0)