Skip to content

56-display-lat-lon-and-elevation-in-2d-profile-measurement #57

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
/>
<link rel="stylesheet" href="/src/AnnotationControl/annotationPanel.css" />
<link rel="stylesheet" href="src/Filter/filter.css" />
<link rel="stylesheet" href="src/2DProfileOverride/2DProfileOverride.css" />
</head>

<body>
Expand Down
12 changes: 12 additions & 0 deletions src/2DProfileOverride/2DProfileOverride.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Keep profile SVG content (axes/labels) from being clipped */
#profileSVG {
overflow: visible !important;
}

/* Improve axis label readability against varying backgrounds */
#profile_window .axis text {
paint-order: stroke;
stroke: #000;
stroke-width: 3px;
stroke-linejoin: round;
}
224 changes: 224 additions & 0 deletions src/2DProfileOverride/2DProfileOverride.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { ecef, wgs84 } from '../config.js'

/**
* Initialize runtime overrides for Potree 2D profile behavior.
* - Patches ProfileWindow.addPoints: converts each point's Z to elevation instead.
* - Rewrites selection info table to show lon/lat/elevation.
*/
export function init2DProfileOverride(viewer) {
const tryPatchAddPoints = (target) => {
if (!target || target.__elevationPatchApplied) return false
const originalAddPoints = target.addPoints
if (typeof originalAddPoints !== 'function') return false

target.addPoints = function patchedAddPoints(pointcloud, points) {
try {
if (!points || !points.data || !points.data.position) {
return originalAddPoints.call(this, pointcloud, points)
}

const srcPos = points.data.position
// Divide by 3 because positions are [x0,y0,z0, x1,y1,z1, ...]
const count = points.numPoints || Math.floor(srcPos.length / 3)
const posArray = srcPos.constructor || Float32Array

// Clone without changing original buffers
const cloned = {
...points,
data: { ...points.data, position: new posArray(srcPos) }
}
const dstPos = cloned.data.position

const pcx = pointcloud?.position?.x || 0
const pcy = pointcloud?.position?.y || 0
const pcz = pointcloud?.position?.z || 0

// Preserve world ECEF Z per point (best-effort). Some Potree UIs attach
// selectedPoint from these attributes; we keep it on the cloned structure
// for later retrieval in the selection panel override.
const ecefZWorld = new Float64Array(count)

for (let i = 0; i < count; i++) {
const ix = 3 * i
const x = srcPos[ix + 0] + pcx
const y = srcPos[ix + 1] + pcy
const z = srcPos[ix + 2] + pcz

const [, , elevation] = proj4(ecef, wgs84, [x, y, z])
// Internally, Potree adds pointcloud.position.z back later.
dstPos[ix + 2] = elevation - pcz

ecefZWorld[i] = z
}

cloned.data.ecefZWorld = ecefZWorld

const result = originalAddPoints.call(this, pointcloud, cloned)

// Try to tag currently selectedPoint with the original ECEF Z if available
try {
if (
this &&
this.selectedPoint &&
Number.isFinite(this.selectedPoint.index)
) {
const idx = this.selectedPoint.index
if (ecefZWorld && idx >= 0 && idx < ecefZWorld.length) {
this.selectedPoint.ecefZWorld = ecefZWorld[idx]
}
}
} catch {}

return result
} catch (err) {
console.warn(
'2DProfileOverride: failed to apply elevation override',
err
)
return originalAddPoints.call(this, pointcloud, points)
}
}

target.__elevationPatchApplied = true
return true
}

let patched = false
if (viewer && viewer.profileWindow) {
patched = tryPatchAddPoints(viewer.profileWindow)
}
if (window.Potree?.ProfileWindow?.prototype) {
patched =
tryPatchAddPoints(window.Potree.ProfileWindow.prototype) || patched
}

const afterPatched = () => {
attachSelectionInfoOverride(viewer)
}

if (patched) {
afterPatched()
} else {
// Poll until the profile window/prototype is available
let tries = 0
const maxTries = 40
const timer = setInterval(() => {
tries += 1
if (
tryPatchAddPoints(window.Potree?.ProfileWindow?.prototype) ||
(viewer?.profileWindow && tryPatchAddPoints(viewer.profileWindow))
) {
clearInterval(timer)
afterPatched()
} else if (tries >= maxTries) {
clearInterval(timer)
}
}, 250)
}
}

// Rewrites the selection properties table inside the profile window
function attachSelectionInfoOverride(viewer) {
const getInfoEl = () => document.getElementById('profileSelectionProperties')
let infoEl = getInfoEl()
if (!infoEl) {
const obs = new MutationObserver(() => {
infoEl = getInfoEl()
if (infoEl) {
obs.disconnect()
monitorSelectionInfo(viewer, infoEl)
}
})
obs.observe(document.body, { childList: true, subtree: true })
} else {
monitorSelectionInfo(viewer, infoEl)
}
}

function monitorSelectionInfo(viewer, infoEl) {
let scheduled = false
let selfUpdating = false

const updateOnce = () => {
scheduled = false
selfUpdating = true
try {
const table = infoEl.querySelector('table')
if (!table) return
const rows = [...table.querySelectorAll('tr')]
if (rows.length < 3) return

const pw = viewer?.profileWindow
const pos = pw?.viewerPickSphere?.position
const sp = pw?.selectedPoint
if (!pos || !sp) return

// Use preserved ECEF Z if we have it; otherwise fall back to pos.z
const trueZ = Number(sp?.ecefZWorld ?? NaN)
let lon, lat
if (Number.isFinite(trueZ)) {
;[lon, lat] = proj4(ecef, wgs84, [pos.x, pos.y, trueZ])
} else {
;[lon, lat] = proj4(ecef, wgs84, [pos.x, pos.y, pos.z])
}
const elevation = pos.z

const setRow = (row, label, val) => {
const tds = row.querySelectorAll('td')
if (tds[0] && tds[0].textContent !== label) tds[0].textContent = label
if (tds[1]) {
const txt = Number.isFinite(val)
? val.toLocaleString(undefined, {
minimumFractionDigits: 4,
maximumFractionDigits: 4
})
: ''
if (tds[1].textContent !== txt) tds[1].textContent = txt
}
}

setRow(rows[0], 'lon', lon)
setRow(rows[1], 'lat', lat)
setRow(rows[2], 'elevation', elevation)

// Remove unwanted rows from the hover info table
const labelsToHide = new Set([
'intensity',
'return number',
'number of returns',
'classification flags',
'classification',
'user data',
'scan angle',
'gps time',
'gps-time',
'rgba'
])

// iterate over a snapshot to avoid issues while removing
const allRows = [...table.querySelectorAll('tr')]
for (let i = 3; i < allRows.length; i++) {
const row = allRows[i]
const labelCell = row.querySelector('td')
const label = (labelCell?.textContent || '').trim().toLowerCase()
if (labelsToHide.has(label)) {
row.remove()
}
}
} finally {
setTimeout(() => {
selfUpdating = false
}, 0)
}
}

const mo = new MutationObserver(() => {
if (selfUpdating) return
if (!scheduled) {
scheduled = true
requestAnimationFrame(updateOnce)
}
})
mo.observe(infoEl, { childList: true, subtree: true })
requestAnimationFrame(updateOnce)
}
4 changes: 4 additions & 0 deletions src/potreeViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js
import { initMiniMap } from './MiniMap/miniMap.js'
import { initFilterPanels, toggleAcceptedLegend } from './Filter/filter.js'
import { ecef } from './config.js'
import { init2DProfileOverride } from './2DProfileOverride/2DProfileOverride.js'

/**
* Initializes the Potree viewer used to visualize the point cloud.
Expand Down Expand Up @@ -125,6 +126,9 @@ export async function createPotreeViewer(
// Show compass
viewer.compass.setVisible(true)

// Apply runtime overrides for the 2D Profile tool
init2DProfileOverride(viewer)

initMeasurementsPanel(viewer)
initAnnotationsPanel(viewer)
initMiniMap(viewer)
Expand Down