Skip to content

Commit

Permalink
Merge pull request #57 from TDT4290-group-4/56-display-latlon-and-ele…
Browse files Browse the repository at this point in the history
…vation-in-2d-profile-measurement

56-display-lat-lon-and-elevation-in-2d-profile-measurement
  • Loading branch information
gautegf authored Nov 2, 2025
2 parents 923d8d6 + 4e0b45e commit c138179
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
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

0 comments on commit c138179

Please sign in to comment.