|
| 1 | +--- |
| 2 | +title: Using Cesium for display of remote parquet. |
| 3 | +categories: [parquet, spatial, recipe] |
| 4 | +--- |
| 5 | + |
| 6 | +This page renders points from an iSamples parquet file on cesium using point primitives. |
| 7 | + |
| 8 | +<script src="https://cesium.com/downloads/cesiumjs/releases/1.127/Build/Cesium/Cesium.js"></script> |
| 9 | +<link href="https://cesium.com/downloads/cesiumjs/releases/1.127/Build/Cesium/Widgets/widgets.css" rel="stylesheet"></link> |
| 10 | +<style> |
| 11 | + div.cesium-topleft { |
| 12 | + display: block; |
| 13 | + position: absolute; |
| 14 | + background: #00000099; |
| 15 | + color: white; |
| 16 | + height: auto; |
| 17 | + z-index: 999; |
| 18 | + } |
| 19 | + #cesiumContainer { |
| 20 | + aspect-ratio: 1/1; |
| 21 | + } |
| 22 | +</style> |
| 23 | + |
| 24 | +```{ojs} |
| 25 | +//| output: false |
| 26 | +Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwNzk3NjkyMy1iNGI1LTRkN2UtODRiMy04OTYwYWE0N2M3ZTkiLCJpZCI6Njk1MTcsImlhdCI6MTYzMzU0MTQ3N30.e70dpNzOCDRLDGxRguQCC-tRzGzA-23Xgno5lNgCeB4'; |
| 27 | +``` |
| 28 | + |
| 29 | +```{ojs} |
| 30 | +//| echo: false |
| 31 | +viewof nodes_path = Inputs.text({ |
| 32 | + label:"Source", |
| 33 | + value:"https://s3.beehivebeach.com/isamples-data/oc_nodes.parquet", |
| 34 | + width:"100%", |
| 35 | + submit:true |
| 36 | +}); |
| 37 | +viewof edges_path = Inputs.text({ |
| 38 | + label:"Source", |
| 39 | + value:"https://s3.beehivebeach.com/isamples-data/oc_edge.parquet", |
| 40 | + width:"100%", |
| 41 | + submit:true |
| 42 | +}); |
| 43 | +``` |
| 44 | + |
| 45 | +```{ojs} |
| 46 | +//| code-fold: true |
| 47 | +
|
| 48 | +// Create a DuckDB instance |
| 49 | +db = { |
| 50 | + const instance = await DuckDBClient.of(); |
| 51 | + await instance.query(`create view nodes as select * from read_parquet('${nodes_path}')`); |
| 52 | + instance.query(`create table edges as select s,p,o from read_parquet('${edges_path}')`); |
| 53 | + return instance; |
| 54 | +} |
| 55 | +
|
| 56 | +
|
| 57 | +async function loadData(query, params=[], waiting_id=null) { |
| 58 | + // Get loading indicator |
| 59 | + const waiter = document.getElementById(waiting_id); |
| 60 | + if (waiter) { |
| 61 | + waiter.hidden = false; |
| 62 | + } |
| 63 | + try { |
| 64 | + // Run the (slow) query |
| 65 | + const _results = await db.query(query, ...params); |
| 66 | + return _results; |
| 67 | + } catch (error) { |
| 68 | + if (waiter) { |
| 69 | + waiter.innerHtml = `<pre>${error}</pre>`; |
| 70 | + } |
| 71 | + return null; |
| 72 | + } finally { |
| 73 | + // Hide the waiter (if there is one) |
| 74 | + if (waiter) { |
| 75 | + waiter.hidden = true; |
| 76 | + } |
| 77 | + } |
| 78 | +} |
| 79 | +
|
| 80 | +locations = { |
| 81 | + // get the content form the parquet file |
| 82 | + const query = `SELECT pid, latitude, longitude FROM nodes WHERE otype='GeospatialCoordLocation'`; |
| 83 | + const data = await loadData(query, [], "loading_1"); |
| 84 | +
|
| 85 | + // Clear the existing PointPrimitiveCollection |
| 86 | + content.points.removeAll(); |
| 87 | + //content.points = new Cesium.PointPrimitiveCollection(); |
| 88 | +
|
| 89 | + // create point primitives for cesium display |
| 90 | + const scalar = new Cesium.NearFarScalar(1.5e2, 2, 8.0e6, 0.2); |
| 91 | + const color = Cesium.Color.PINK; |
| 92 | + const point_size = 4; |
| 93 | + for (const row of data) { |
| 94 | + content.points.add({ |
| 95 | + id: row.pid, |
| 96 | + // https://cesium.com/learn/cesiumjs/ref-doc/Cartesian3.html#.fromDegrees |
| 97 | + position: Cesium.Cartesian3.fromDegrees( |
| 98 | + row.longitude, //longitude |
| 99 | + row.latitude, //latitude |
| 100 | + 0,//randomCoordinateJitter(10.0, 10.0), //elevation, m |
| 101 | + ), |
| 102 | + pixelSize: point_size, |
| 103 | + color: color, |
| 104 | + scaleByDistance: scalar, |
| 105 | + }); |
| 106 | + } |
| 107 | + content.enableTracking(); |
| 108 | + return data; |
| 109 | +} |
| 110 | +
|
| 111 | +
|
| 112 | +function createShowPrimitive(viewer) { |
| 113 | + return function(movement) { |
| 114 | + // Get the point at the mouse end position |
| 115 | + const selectPoint = viewer.viewer.scene.pick(movement.endPosition); |
| 116 | +
|
| 117 | + // Clear the current selection, if there is one and it is different to the selectPoint |
| 118 | + if (viewer.currentSelection !== null) { |
| 119 | + //console.log(`selected.p ${viewer.currentSelection}`) |
| 120 | + if (Cesium.defined(selectPoint) && selectPoint !== viewer.currentSelection) { |
| 121 | + console.log(`selected.p 2 ${viewer.currentSelection}`) |
| 122 | + viewer.currentSelection.primitive.pixelSize = 4; |
| 123 | + viewer.currentSelection.primitive.outlineColor = Cesium.Color.TRANSPARENT; |
| 124 | + viewer.currentSelection.outlineWidth = 0; |
| 125 | + viewer.currentSelection = null; |
| 126 | + } |
| 127 | + } |
| 128 | +
|
| 129 | + // If selectPoint is valid and no currently selected point |
| 130 | + if (Cesium.defined(selectPoint) && selectPoint.hasOwnProperty("primitive")) { |
| 131 | + //console.log(`showPrimitiveId ${selectPoint.id}`); |
| 132 | + //const carto = Cesium.Cartographic.fromCartesian(selectPoint.primitive.position) |
| 133 | + viewer.pointLabel.position = selectPoint.primitive.position; |
| 134 | + viewer.pointLabel.label.show = true; |
| 135 | + //viewer.pointLabel.label.text = `id:${selectPoint.id}, ${carto}`; |
| 136 | + viewer.pointLabel.label.text = `${selectPoint.id}`; |
| 137 | + selectPoint.primitive.pixelSize = 20; |
| 138 | + selectPoint.primitive.outlineColor = Cesium.Color.YELLOW; |
| 139 | + selectPoint.primitive.outlineWidth = 3; |
| 140 | + viewer.currentSelection = selectPoint; |
| 141 | + } else { |
| 142 | + viewer.pointLabel.label.show = false; |
| 143 | + } |
| 144 | + } |
| 145 | +} |
| 146 | +
|
| 147 | +class CView { |
| 148 | + constructor(target) { |
| 149 | + this.viewer = new Cesium.Viewer( |
| 150 | + target, { |
| 151 | + timeline: false, |
| 152 | + animation: false, |
| 153 | + baseLayerPicker: false, |
| 154 | + fullscreenElement: target, |
| 155 | + terrain: Cesium.Terrain.fromWorldTerrain() |
| 156 | + }); |
| 157 | + this.currentSelection = null; |
| 158 | + this.point_size = 1; |
| 159 | + this.n_points = 0; |
| 160 | + // https://cesium.com/learn/cesiumjs/ref-doc/PointPrimitiveCollection.html |
| 161 | + this.points = new Cesium.PointPrimitiveCollection(); |
| 162 | + this.viewer.scene.primitives.add(this.points); |
| 163 | + |
| 164 | + this.pointLabel = this.viewer.entities.add({ |
| 165 | + label: { |
| 166 | + show: false, |
| 167 | + showBackground: true, |
| 168 | + font: "14px monospace", |
| 169 | + horizontalOrigin: Cesium.HorizontalOrigin.LEFT, |
| 170 | + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| 171 | + pixelOffset: new Cesium.Cartesian2(15, 0), |
| 172 | + // this attribute will prevent this entity clipped by the terrain |
| 173 | + disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| 174 | + text:"", |
| 175 | + }, |
| 176 | + }); |
| 177 | +
|
| 178 | + this.pickHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); |
| 179 | + // Can also do this rather than wait for the points to be generated |
| 180 | + //this.pickHandler.setInputAction(createShowPrimitive(this), Cesium.ScreenSpaceEventType.MOUSE_MOVE); |
| 181 | +
|
| 182 | + this.selectHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); |
| 183 | + this.selectHandler.setInputAction((e) => { |
| 184 | + const selectPoint = this.viewer.scene.pick(e.position); |
| 185 | + if (Cesium.defined(selectPoint) && selectPoint.hasOwnProperty("primitive")) { |
| 186 | + mutable clickedPointId = selectPoint.id; |
| 187 | + } |
| 188 | + },Cesium.ScreenSpaceEventType.LEFT_CLICK); |
| 189 | +
|
| 190 | + } |
| 191 | +
|
| 192 | + enableTracking() { |
| 193 | + this.pickHandler.setInputAction(createShowPrimitive(this), Cesium.ScreenSpaceEventType.MOUSE_MOVE); |
| 194 | + } |
| 195 | +} |
| 196 | +
|
| 197 | +content = new CView("cesiumContainer"); |
| 198 | +
|
| 199 | +async function getGeoRecord(pid) { |
| 200 | + if (pid === null || pid ==="" || pid == "unset") { |
| 201 | + return "unset"; |
| 202 | + } |
| 203 | + const q = `SELECT row_id, pid, otype, latitude, longitude FROM nodes WHERE otype='GeospatialCoordLocation' AND pid=?`; |
| 204 | + const result = await db.queryRow(q, [pid]); |
| 205 | + return result; |
| 206 | +} |
| 207 | +
|
| 208 | +async function locationUsedBy(rowid){ |
| 209 | + if (rowid === undefined || rowid === null) { |
| 210 | + return []; |
| 211 | + } |
| 212 | + const q = `select pid, otype from nodes where row_id in (select edges.s from edges where edges.o=?);`; |
| 213 | + return db.query(q, [rowid]); |
| 214 | +} |
| 215 | +
|
| 216 | +async function samplesAtLocation(rowid) { |
| 217 | + if (rowid === undefined || rowid === null) { |
| 218 | + return []; |
| 219 | + } |
| 220 | + const q = `select pid, label, description from nodes where row_id in ( |
| 221 | + with recursive efor(s,p,o) as ( |
| 222 | + select s,p,o from edges where o=? |
| 223 | + union all |
| 224 | + select e.s, e.p, e.o from edges as e, efor as ef where ef.s = e.o |
| 225 | + ) select s from efor where p='produced_by');`; |
| 226 | + return db.query(q, [rowid]); |
| 227 | +} |
| 228 | +
|
| 229 | +mutable clickedPointId = "unset"; |
| 230 | +selectedGeoRecord = await getGeoRecord(clickedPointId); |
| 231 | +
|
| 232 | +md`Retrieved ${pointdata.length} locations from ${nodes_path}.`; |
| 233 | +``` |
| 234 | + |
| 235 | +::: {.panel-tabset} |
| 236 | + |
| 237 | +## Map |
| 238 | + |
| 239 | +<div id="cesiumContainer"></div> |
| 240 | + |
| 241 | +## Data |
| 242 | + |
| 243 | +<div id="loading_1">Loading...</div> |
| 244 | + |
| 245 | +```{ojs} |
| 246 | +//| code-fold: true |
| 247 | +
|
| 248 | +viewof pointdata = { |
| 249 | + const data_table = Inputs.table(locations, { |
| 250 | + header: { |
| 251 | + row_id:"Row ID", |
| 252 | + pid: "PID", |
| 253 | + latitude: "Latitude", |
| 254 | + longitude: "Longitude" |
| 255 | + }, |
| 256 | + }); |
| 257 | + return data_table; |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +::: |
| 262 | + |
| 263 | +The click point ID is "${clickedPointId}". |
| 264 | + |
| 265 | +```{ojs} |
| 266 | +//| echo: false |
| 267 | +md`\`\`\` |
| 268 | +${JSON.stringify(selectedGeoRecord, null, 2)} |
| 269 | +\`\`\` |
| 270 | +` |
| 271 | +``` |
| 272 | + |
| 273 | +```{ojs} |
| 274 | +Inputs.table(locationUsedBy(selectedGeoRecord.row_id)); |
| 275 | +``` |
| 276 | + |
| 277 | +```{ojs} |
| 278 | +viewof usedby = { |
| 279 | + const table = Inputs.table(samplesAtLocation(selectedGeoRecord.row_id)); |
| 280 | + return table; |
| 281 | +} |
| 282 | +``` |
0 commit comments