diff --git a/tutorials/parquet_cesium.qmd b/tutorials/parquet_cesium.qmd index 0f271b2..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,78 +784,93 @@ md`Retrieved ${pointdata.length} locations from ${parquet_path}.`; ```{ojs} //| echo: false +//| output: false // Handle optional classification button: recolor dots by type { if (classifyDots !== null) { 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 + } } } ``` @@ -914,6 +931,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 +1342,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}