diff --git a/README.md b/README.md index 625f104..0e480c8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Molloy Explorer is a 3D seabed visualization tool built with **Potree**. It allo ### Add point cloud data -Place the point cloud data (in Potree format with EPSG:4978 coordinates) in `public/pointclouds/data_converted`. +Place the point cloud data (in Potree format with EPSG:4978 coordinates) in `public/pointclouds`. Ensure the point cloud folder names match the paths specified in `src/config.js`, either by renaming the point cloud folders or by updating the paths. **Note:** Point cloud files should not be committed to Git. diff --git a/src/AcceptedFiltering/threePanels.js b/src/AcceptedFiltering/threePanels.js index 2ab84f4..249df0f 100644 --- a/src/AcceptedFiltering/threePanels.js +++ b/src/AcceptedFiltering/threePanels.js @@ -45,6 +45,17 @@ function insertSection({ headerId, headerText, listId }) { } accordionRefresh() + + header.addEventListener('click', () => { + const $ = window.jQuery || window.$ + if ($ && $.fn?.accordion && $(menu).data('uiAccordion')) return + if ($) { + const $p = $(panel) + $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350) + return + } + panel.style.display = panel.style.display === 'none' ? '' : 'none' + }) } /** @@ -181,10 +192,10 @@ function ensureElevationButton(hooks) { } /** - * Reconnects the Elevation slider label to reflect the current range. - * Assumes #sldHeightRange and #lblHeightRange exist. + * Sets up a elevation range slider for interactive updates + * @param {{onElevationRangeChange?:Function}} hooks */ -function rebindElevationLabel() { +function setUpElevationSlider(hooks) { const $ = window.jQuery || window.$ const slider = $ ? $('#sldHeightRange') : null const label = byId('lblHeightRange') @@ -194,11 +205,12 @@ function rebindElevationLabel() { const low = slider.slider('values', 0) const high = slider.slider('values', 1) label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}` + hooks?.onElevationRangeChange([low, high]) } slider.slider({ min: -10000, max: 0, values: [-10000, 0] }) slider.off('slide.custom slidestop.custom change.custom') - slider.on('slide.custom', 'slidestop.custom change.custom', update) + slider.on('slide.custom slidestop.custom change.custom', update) update() } @@ -206,14 +218,14 @@ function rebindElevationLabel() { * Moves Potree's Elevation container under the Elevation panel body and rebinds label. * @returns {boolean} true if moved or already in place */ -function moveElevationContainer() { +function moveElevationContainer(hooks) { const { body } = ensurePanelScaffold('elevation2_list') const src = byId('materials.elevation_container') if (!body || !src) return false if (src.parentNode !== body) { body.appendChild(src) - rebindElevationLabel() + setUpElevationSlider(hooks) accordionRefresh() } return true @@ -229,7 +241,7 @@ function initElevationControls(hooks) { const root = byId('potree_menu') || document.body const obs = new MutationObserver(() => { - if (byId('materials.elevation_container')) moveElevationContainer() + if (byId('materials.elevation_container')) moveElevationContainer(hooks) }) obs.observe(root, { childList: true, subtree: true }) } @@ -362,7 +374,7 @@ async function ensurePanelCaptured(mode, hooks) { selectCloudNode(hooks) src = await waitForOrPoll('materials.elevation_container', 1800) } - if (src) moveElevationContainer() + if (src) moveElevationContainer(hooks) return } } @@ -394,14 +406,14 @@ async function switchMode(mode, hook, hooksBag = {}) { * @param {()=>'elevation'|'accepted'} activeGetter * @returns {MutationObserver} */ -function attachSelfHealing(activeGetter) { +function attachSelfHealing(activeGetter, hooks) { const root = byId('potree_menu') || document.body const obs = new MutationObserver(() => { const mode = activeGetter() if (mode === 'elevation') { const src = byId('materials.elevation_container') const { body } = ensurePanelScaffold('elevation2_list') - if (src && body && src.parentNode !== body) moveElevationContainer() + if (src && body && src.parentNode !== body) moveElevationContainer(hooks) } }) obs.observe(root, { childList: true, subtree: true }) @@ -412,7 +424,7 @@ function attachSelfHealing(activeGetter) { /** * Public entrypoint: builds Elevation and Accepted panels and wires behavior. * @param {object} viewer Potree viewer (not used directly here but available to hooks) - * @param {{onActivateElevation?:Function, onActivateAccepted?:Function, selectCloudOnce?:Function}} hooks + * @param {{onActivateElevation?:Function, onActivateAccepted?:Function, selectCloudOnce?:Function, onElevationRangeChange?:Function}} hooks */ export function initThreePanels(viewer, hooks = {}) { // Build sections @@ -433,7 +445,7 @@ export function initThreePanels(viewer, hooks = {}) { setActive('accepted') ) - attachSelfHealing(getActive) + attachSelfHealing(getActive, hooks) // Default: auto-activate Elevation once clickOnce('btnDoElevationControl') diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 4bdd4ec..cffe313 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -56,12 +56,14 @@ export function initAnnotationsPanel(viewer) { // Toggle collapse header.addEventListener('click', () => { - if ($(menu).accordion && $(menu).data('uiAccordion')) return - if (window.jQuery) { - const $p = window.jQuery(panel) + const $ = window.jQuery || window.$ + if ($ && $.fn?.accordion && $(menu).data('uiAccordion')) return + if ($) { + const $p = $(panel) $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350) return } + panel.style.display = panel.style.display === 'none' ? '' : 'none' }) targetContainer = panel.querySelector('#annotations_list') } diff --git a/src/config.js b/src/config.js index 09c17aa..761f8fd 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,20 @@ -export const POTREE_POINTCLOUD_URL = '/pointclouds/data_converted/metadata.json' +export const POTREE_POINTCLOUD_URLS = [ + '/pointclouds/cell_1/metadata.json', + '/pointclouds/cell_2/metadata.json', + '/pointclouds/cell_3/metadata.json', + '/pointclouds/cell_4/metadata.json', + '/pointclouds/cell_5/metadata.json', + '/pointclouds/cell_6/metadata.json', + '/pointclouds/cell_7/metadata.json', + '/pointclouds/cell_8/metadata.json', + '/pointclouds/cell_9/metadata.json', + '/pointclouds/cell_10/metadata.json', + '/pointclouds/cell_11/metadata.json', + '/pointclouds/cell_12/metadata.json', + '/pointclouds/cell_13/metadata.json', + '/pointclouds/cell_14/metadata.json', + '/pointclouds/cell_15/metadata.json' +] export const POTREE_SETTINGS = { edl: true, diff --git a/src/main.js b/src/main.js index e11565e..87aaafe 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import { POTREE_POINTCLOUD_URL, POTREE_SETTINGS } from './config.js' +import { POTREE_POINTCLOUD_URLS, POTREE_SETTINGS } from './config.js' import { createCesiumViewer, loadCountryBorders } from './cesiumViewer.js' import { createPotreeViewer } from './potreeViewer.js' import { syncCameras } from './cameraSync.js' @@ -18,7 +18,7 @@ async function init() { window.potreeViewer = await createPotreeViewer( 'potree_render_area', - POTREE_POINTCLOUD_URL, + POTREE_POINTCLOUD_URLS, POTREE_SETTINGS ) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 3d80cf0..a4903c1 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -10,11 +10,15 @@ import { ecef } from './config.js' * Initializes the Potree viewer used to visualize the point cloud. * * @param containerId - id of the container - * @param pointcloudUrl - url path to the point cloud + * @param pointcloudUrls - url paths to the point clouds * @param settings - other settings * @returns Potree viewer */ -export async function createPotreeViewer(containerId, pointcloudUrl, settings) { +export async function createPotreeViewer( + containerId, + pointcloudUrls, + settings +) { const viewer = new Potree.Viewer(document.getElementById(containerId), { useDefaultRenderLoop: false }) @@ -31,6 +35,26 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { viewer.loadSettingsFromURL() viewer.setDescription('Molloy Explorer') + const pointclouds = [] + for (const url of pointcloudUrls) { + const e = await Potree.loadPointCloud(url) + const pc = e.pointcloud + viewer.scene.addPointCloud(pc) + + pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE + pc.material.shape = Potree.PointShape.CIRCLE + overrideShaderForGradient(pc) + + //The default activeAttributeName is set to elevation and the color gradient to VIRIDIS for good visualization + pc.material.elevationRange = [-10000, 0] + pc.material.activeAttributeName = 'elevation' + pc.material.gradient = Potree.Gradients['VIRIDIS'] + + e.pointcloud.projection = ecef + + pointclouds.push(pc) + } + viewer.loadGUI(() => { viewer.setLanguage('en') $('#menu_appearance').next().show() @@ -39,22 +63,53 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { $('#menu_filters').next().show() viewer.toggleSidebar() + // Store the last used elevation gradient + let lastElevationGradient = 'VIRIDIS' + + // Helper function to update all point clouds' elevation range + function updateAllCloudsElevation(range) { + pointclouds.forEach((pc) => { + pc.material.activeAttributeName = 'elevation' + pc.material.elevationRange = range + }) + } + + // Helper function to update all point clouds' gradient + function updateAllCloudsGradient(gradientName) { + pointclouds.forEach((pc) => { + pc.material.gradient = Potree.Gradients[gradientName] + }) + } + + // Helper function to update all point clouds for Accepted filtering + function updateAllCloudsAccepted(gradientName) { + pointclouds.forEach((pc) => { + pc.material.activeAttributeName = 'Accepted' + pc.material.gradient = Potree.Gradients[gradientName] + }) + } + initThreePanels(viewer, { onActivateElevation: () => { - if (!pc) return - pc.material.activeAttributeName = 'elevation' - pc.material.gradient = Potree.Gradients['VIRIDIS'] + const $ = window.jQuery || window.$ + const slider = $ ? $('#sldHeightRange') : null + const values = slider?.slider('values') ?? [] + const low = typeof values[0] === 'number' ? values[0] : -10000 + const high = typeof values[1] === 'number' ? values[1] : 0 + + updateAllCloudsElevation([low, high]) + updateAllCloudsGradient(lastElevationGradient) suppressSidebarAutoScroll(clickCloudIconOnce) }, onActivateAccepted: () => { - if (!pc) return - pc.material.activeAttributeName = 'accepted' - pc.material.gradient = Potree.Gradients['GRAYSCALE'] + updateAllCloudsAccepted('GRAYSCALE') toggleAcceptedLegend(true) suppressSidebarAutoScroll(clickCloudIconOnce) - } + }, + onElevationRangeChange: updateAllCloudsElevation }) - // // // helper + + // helper function clickCloudIconOnce() { const icon = document.querySelector( '#scene_objects i.jstree-themeicon-custom' @@ -62,31 +117,17 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { if (icon) icon.dispatchEvent(new MouseEvent('click', { bubbles: true })) } + function setLastElevationGradient(gradientName) { + lastElevationGradient = gradientName + } + overrideGradientSchemeClick(pointclouds, setLastElevationGradient) + + makeGlobeBackgroundOption() + initMeasurementsPanel(viewer) initAnnotationsPanel(viewer) }) - const e = await Potree.loadPointCloud(pointcloudUrl) - const pc = e.pointcloud - viewer.scene.addPointCloud(pc) - - // Change name of default background from 'None' to 'Globe"' - $('#background_options_none') - .text('Globe') - .attr('id', 'background_options_globe') - .val('globe') - - viewer.setBackground('globe') - - pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE - pc.material.shape = Potree.PointShape.CIRCLE - overrideShaderForGradient(pc) - - //The default activeAttributeName is set to elevation and the color gradient to VIRIDIS for good visualization - pc.material.elevationRange = [-10000, 0] - pc.material.activeAttributeName = 'elevation' - pc.material.gradient = Potree.Gradients['VIRIDIS'] - // Initialize camera position and target point (manually chosen) viewer.scene.view.setView( [3961574.044, 1494736.334, 8348318.575], // Initial camera position @@ -259,3 +300,48 @@ function suppressSidebarAutoScroll(action, holdMs = 350) { requestAnimationFrame(restoreLoop) } } + +/** + * Overrides the click event handlers for gradient scheme selection to apply + * gradients to multiple point clouds. + * + * @param pointclouds - Array of point cloud objects + * @param {Function} setLastElevationGradient - Callback function to store the last selected gradient name + */ +function overrideGradientSchemeClick(pointclouds, setLastElevationGradient) { + const gradientContainer = document.getElementById( + 'elevation_gradient_scheme_selection' + ) + if (!gradientContainer) return + const spans = gradientContainer.querySelectorAll('span') + if (spans.length) { + spans.forEach((span, idx) => { + span.addEventListener('click', () => { + const gradientNames = Object.keys(Potree.Gradients) + const gradientName = gradientNames[idx] + if (gradientName) { + pointclouds.forEach((pc) => { + pc.material.gradient = Potree.Gradients[gradientName] + }) + setLastElevationGradient(gradientName) + } + }) + }) + } +} + +/** + * Converts the "None" background option to a "Globe" background option and sets it as the default. + */ +function makeGlobeBackgroundOption() { + const bgInput = document.getElementById('background_options_none') + const bgLabel = document.querySelector('label[for="background_options_none"]') + + if (bgInput && bgLabel) { + bgLabel.textContent = 'Globe' + bgInput.id = 'background_options_globe' + bgInput.value = 'globe' + bgLabel.setAttribute('for', 'background_options_globe') + bgLabel.click() + } +}