From 87fdceacc74ab448e242efefadbf17b29d705515 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Sat, 1 Nov 2025 10:48:30 -0700 Subject: [PATCH 1/3] Move output tables above explanations for better UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganized Cesium tutorial to show query results immediately after clicking a point, before technical explanations. Changes: - Moved "Samples at Location" section (HTML table output) directly after "getGeoRecord (selected)" section - "Understanding Paths in the iSamples Property Graph" now appears after results - Removed duplicate output section from old location Benefits: - Users see results immediately without scrolling through explanations - Technical details remain available below for those interested - Cleaner, more intuitive information hierarchy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tutorials/parquet_cesium.qmd | 215 +++++++++++++++++------------------ 1 file changed, 107 insertions(+), 108 deletions(-) diff --git a/tutorials/parquet_cesium.qmd b/tutorials/parquet_cesium.qmd index 0f271b2..e73b549 100644 --- a/tutorials/parquet_cesium.qmd +++ b/tutorials/parquet_cesium.qmd @@ -914,6 +914,113 @@ ${JSON.stringify(testrecord, null, 2)} ` ``` +## Samples at Location via Sampling Event (Eric Kansa's Query) + + + +This query implements Eric Kansa's authoritative `get_samples_at_geo_cord_location_via_sample_event` function from [open-context-py](https://github.com/ekansa/open-context-py/blob/staging/opencontext_py/apps/all_items/isamples/isamples_explore.py). + +**Query Strategy (Path 1 Only)**: +- Starts at a GeospatialCoordLocation (clicked point) +- Walks **backward** via `sample_location` edges to find SamplingEvents that reference this location +- From those events, finds MaterialSampleRecords produced by them +- Requires site context (INNER JOIN on `sampling_site` → SamplingSite) + +**Returns**: +- Geographic coordinates: `latitude`, `longitude` +- Sample metadata: `sample_pid`, `sample_label`, `sample_description`, `sample_alternate_identifiers` +- Site context: `sample_site_label`, `sample_site_pid` +- Media: `sample_thumbnail_url`, `has_thumbnail` + +**Ordering**: Prioritizes samples with images (`ORDER BY has_thumbnail DESC`) + +**Important**: This query only returns samples whose **sampling events directly reference this geolocation** via `sample_location` (Path 1). Samples that reach this location only through their site's `site_location` (Path 2) are **not included**. This means site marker locations may return 0 results if no events were recorded at that exact coordinate. + +```{ojs} +//| echo: false +samples_combined = selectedSamplesCombined +``` + +```{ojs} +//| echo: false +html`${ + combinedLoading ? + html`
Loading samples…
` + : + samples_combined && samples_combined.length > 0 ? + html`
+ + + + + + + + + + + + ${samples_combined.map((sample, i) => html` + + + + + + + + `)} + +
ThumbnailSampleDescriptionSiteLocation
+ ${sample.has_thumbnail ? + html` + ${sample.sample_label} + ` + : + html`
No image
` + } +
+
+ ${sample.sample_label} +
+ +
+
+ ${sample.sample_description || 'No description'} +
+
+
+ ${sample.sample_site_label} +
+ +
+ ${sample.latitude.toFixed(5)}°N
+ ${sample.longitude.toFixed(5)}°E +
+
+
+ Found ${samples_combined.length} sample${samples_combined.length !== 1 ? 's' : ''} +
` + : + html`
+ No samples found at this location via Path 1 (direct sampling events). +
` +}` +``` + ## Understanding Paths in the iSamples Property Graph ### Why "Path 1" and "Path 2"? @@ -1218,114 +1325,6 @@ html`${ }` ``` - -## Samples at Location via Sampling Event (Eric Kansa's Query) - - - -This query implements Eric Kansa's authoritative `get_samples_at_geo_cord_location_via_sample_event` function from [open-context-py](https://github.com/ekansa/open-context-py/blob/staging/opencontext_py/apps/all_items/isamples/isamples_explore.py). - -**Query Strategy (Path 1 Only)**: -- Starts at a GeospatialCoordLocation (clicked point) -- Walks **backward** via `sample_location` edges to find SamplingEvents that reference this location -- From those events, finds MaterialSampleRecords produced by them -- Requires site context (INNER JOIN on `sampling_site` → SamplingSite) - -**Returns**: -- Geographic coordinates: `latitude`, `longitude` -- Sample metadata: `sample_pid`, `sample_label`, `sample_description`, `sample_alternate_identifiers` -- Site context: `sample_site_label`, `sample_site_pid` -- Media: `sample_thumbnail_url`, `has_thumbnail` - -**Ordering**: Prioritizes samples with images (`ORDER BY has_thumbnail DESC`) - -**Important**: This query only returns samples whose **sampling events directly reference this geolocation** via `sample_location` (Path 1). Samples that reach this location only through their site's `site_location` (Path 2) are **not included**. This means site marker locations may return 0 results if no events were recorded at that exact coordinate. - -```{ojs} -//| echo: false -samples_combined = selectedSamplesCombined -``` - -```{ojs} -//| echo: false -html`${ - combinedLoading ? - html`
Loading samples…
` - : - samples_combined && samples_combined.length > 0 ? - html`
- - - - - - - - - - - - ${samples_combined.map((sample, i) => html` - - - - - - - - `)} - -
ThumbnailSampleDescriptionSiteLocation
- ${sample.has_thumbnail ? - html` - ${sample.sample_label} - ` - : - html`
No image
` - } -
-
- ${sample.sample_label} -
- -
-
- ${sample.sample_description || 'No description'} -
-
-
- ${sample.sample_site_label} -
- -
- ${sample.latitude.toFixed(5)}°N
- ${sample.longitude.toFixed(5)}°E -
-
-
- Found ${samples_combined.length} sample${samples_combined.length !== 1 ? 's' : ''} -
` - : - html`
- No samples found at this location via Path 1 (direct sampling events). -
` -}` -``` - ## Geographic Location Classification ::: {.callout-tip icon=false} From 55ae9d312cff363b6e262d8fe48c39d94d183ad6 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Sat, 1 Nov 2025 12:27:15 -0700 Subject: [PATCH 2/3] Add error handling for classification query failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the color-coding classification query in try-catch to handle intermittent DuckDB-WASM HTTP range request errors when querying large remote parquet files. Changes: - Added try-catch around classification query execution - Logs user-friendly error messages to console instead of crashing - Page remains functional even if classification fails - Provides helpful tips about retrying or using local cached file Fixes occasional "Range request...offset is out of bounds" errors that occur due to DuckDB-WASM limitations with remote files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tutorials/parquet_cesium.qmd | 138 +++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 62 deletions(-) diff --git a/tutorials/parquet_cesium.qmd b/tutorials/parquet_cesium.qmd index e73b549..2dace69 100644 --- a/tutorials/parquet_cesium.qmd +++ b/tutorials/parquet_cesium.qmd @@ -788,72 +788,86 @@ md`Retrieved ${pointdata.length} locations from ${parquet_path}.`; console.log("Classifying dots by type..."); performance.mark('classify-start'); - // Run the classification query - const query = ` - WITH geo_classification AS ( + try { + // Run the classification query + const query = ` + WITH geo_classification AS ( + SELECT + geo.pid, + MAX(CASE WHEN e.p = 'sample_location' THEN 1 ELSE 0 END) as is_sample_location, + MAX(CASE WHEN e.p = 'site_location' THEN 1 ELSE 0 END) as is_site_location + FROM nodes geo + JOIN nodes e ON (geo.row_id = e.o[1]) + WHERE geo.otype = 'GeospatialCoordLocation' + GROUP BY geo.pid + ) SELECT - geo.pid, - MAX(CASE WHEN e.p = 'sample_location' THEN 1 ELSE 0 END) as is_sample_location, - MAX(CASE WHEN e.p = 'site_location' THEN 1 ELSE 0 END) as is_site_location - FROM nodes geo - JOIN nodes e ON (geo.row_id = e.o[1]) - WHERE geo.otype = 'GeospatialCoordLocation' - GROUP BY geo.pid - ) - SELECT - pid, - CASE - WHEN is_sample_location = 1 AND is_site_location = 1 THEN 'both' - WHEN is_sample_location = 1 THEN 'sample_location_only' - WHEN is_site_location = 1 THEN 'site_location_only' - END as location_type - FROM geo_classification - `; - - const classifications = await db.query(query); - - // Build lookup map: pid -> location_type - const typeMap = new Map(); - for (const row of classifications) { - typeMap.set(row.pid, row.location_type); - } + pid, + CASE + WHEN is_sample_location = 1 AND is_site_location = 1 THEN 'both' + WHEN is_sample_location = 1 THEN 'sample_location_only' + WHEN is_site_location = 1 THEN 'site_location_only' + END as location_type + FROM geo_classification + `; + + const classifications = await db.query(query); + + // Build lookup map: pid -> location_type + const typeMap = new Map(); + for (const row of classifications) { + typeMap.set(row.pid, row.location_type); + } - // Color and size styling by location type - const styles = { - sample_location_only: { - color: Cesium.Color.fromCssColorString('#2E86AB'), - size: 3 - }, // Blue - field collection points - site_location_only: { - color: Cesium.Color.fromCssColorString('#A23B72'), - size: 6 - }, // Purple - administrative markers - both: { - color: Cesium.Color.fromCssColorString('#F18F01'), - size: 5 - } // Orange - dual-purpose - }; - - // Update colors of existing points - const points = content.points; - for (let i = 0; i < points.length; i++) { - const point = points.get(i); - const pid = point.id; - const locationType = typeMap.get(pid); - - if (locationType && styles[locationType]) { - point.color = styles[locationType].color; - point.pixelSize = styles[locationType].size; + // Color and size styling by location type + const styles = { + sample_location_only: { + color: Cesium.Color.fromCssColorString('#2E86AB'), + size: 3 + }, // Blue - field collection points + site_location_only: { + color: Cesium.Color.fromCssColorString('#A23B72'), + size: 6 + }, // Purple - administrative markers + both: { + color: Cesium.Color.fromCssColorString('#F18F01'), + size: 5 + } // Orange - dual-purpose + }; + + // Update colors of existing points + const points = content.points; + for (let i = 0; i < points.length; i++) { + const point = points.get(i); + const pid = point.id; + const locationType = typeMap.get(pid); + + if (locationType && styles[locationType]) { + point.color = styles[locationType].color; + point.pixelSize = styles[locationType].size; + } } - } - performance.mark('classify-end'); - performance.measure('classification', 'classify-start', 'classify-end'); - const classifyTime = performance.getEntriesByName('classification')[0].duration; - console.log(`Classification completed in ${classifyTime.toFixed(0)}ms - updated ${points.length} points`); - console.log(` - Blue (sample_location_only): field collection points`); - console.log(` - Purple (site_location_only): administrative markers`); - console.log(` - Orange (both): dual-purpose locations`); + performance.mark('classify-end'); + performance.measure('classification', 'classify-start', 'classify-end'); + const classifyTime = performance.getEntriesByName('classification')[0].duration; + console.log(`Classification completed in ${classifyTime.toFixed(0)}ms - updated ${points.length} points`); + console.log(` - Blue (sample_location_only): field collection points`); + console.log(` - Purple (site_location_only): administrative markers`); + console.log(` - Orange (both): dual-purpose locations`); + } catch (error) { + console.error("Classification failed:", error); + console.error("Error details:", error.message); + + // Show user-friendly message in browser console + console.warn("⚠️ Color-coding failed due to a data loading issue."); + console.warn("💡 Tip: This is an intermittent DuckDB-WASM issue with remote files."); + console.warn(" Try clicking the button again, or use a local cached file for better reliability."); + console.warn(" See the 'Using a local cached file' section above for instructions."); + + // Note: We don't show an alert() to avoid disrupting the user experience + // The page remains functional, just without the color-coding + } } } ``` From 08fe14ff9f0921d648e538eaa06228407d7f504d Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Sat, 1 Nov 2025 13:03:34 -0700 Subject: [PATCH 3/3] Suppress 'undefined' outputs from side-effect Observable cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added //| output: false directive to three Observable cells that execute side effects but don't return values, which were displaying "undefined" on the page. Fixed cells: - Camera initialization (PKAP view setup) - Geocode search handler - Classification button handler These cells only perform side effects (camera control, event handling), so their output should be suppressed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tutorials/parquet_cesium.qmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tutorials/parquet_cesium.qmd b/tutorials/parquet_cesium.qmd index 2dace69..8aa7e76 100644 --- a/tutorials/parquet_cesium.qmd +++ b/tutorials/parquet_cesium.qmd @@ -714,6 +714,7 @@ md`Retrieved ${pointdata.length} locations from ${parquet_path}.`; ```{ojs} //| echo: false +//| output: false // Center initial Cesium view on PKAP Survey Area and also set Home to PKAP! { const viewer = content.viewer; @@ -744,6 +745,7 @@ md`Retrieved ${pointdata.length} locations from ${parquet_path}.`; ```{ojs} //| echo: false +//| output: false // Handle geocode search: fly to location and trigger queries { if (searchGeoPid && searchGeoPid.trim() !== "") { @@ -782,6 +784,7 @@ md`Retrieved ${pointdata.length} locations from ${parquet_path}.`; ```{ojs} //| echo: false +//| output: false // Handle optional classification button: recolor dots by type { if (classifyDots !== null) {