@@ -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
210342md`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