// Global variables let planetData = null; let threejsScene, threejsCamera, threejsRenderer, threejsControls, threejsSphere; let d3Svg, d3Projection, d3Path; let progressInterval; // Screen management function showScreen(screenId) { // Hide all screens document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); // Show the requested screen document.getElementById(screenId).classList.add('active'); // Initialize visualization if showing that screen if (screenId === 'visualization-screen' && planetData) { // Need a small delay for the containers to be visible with correct dimensions setTimeout(() => { if (!threejsScene) { initVisualizations(); } else { // If already initialized, handle resize for containers that were hidden handleResize(); } }, 100); } } // Handle window resize function handleResize() { if (threejsRenderer && threejsCamera) { const container = document.getElementById('threejs-container'); const width = container.clientWidth; const height = container.clientHeight; threejsCamera.aspect = width / height; threejsCamera.updateProjectionMatrix(); threejsRenderer.setSize(width, height); } if (d3Svg && d3Projection) { const container = document.getElementById('d3-container'); const width = container.clientWidth; const height = container.clientHeight; d3Svg.attr('width', width) .attr('height', height); d3Projection.scale(width / (2 * Math.PI) * 0.9) .translate([width / 2, height / 1.8]); updateVisualization(); } } // Initialize the 3D visualization with Three.js function init3DVisualization() { const container = document.getElementById('threejs-container'); const width = container.clientWidth; const height = container.clientHeight; // Create scene threejsScene = new THREE.Scene(); threejsScene.background = new THREE.Color(0x000000); // Create camera threejsCamera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); threejsCamera.position.z = 2; // Add polar axis helper const axisHelper = new THREE.Group(); // North-South pole axis (red) const poleGeometry = new THREE.CylinderGeometry(0.01, 0.01, 2.4, 8); const poleMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const poleAxis = new THREE.Mesh(poleGeometry, poleMaterial); // Correctly align with y-axis (poles should be on y-axis) poleAxis.rotation.z = 0; axisHelper.add(poleAxis); // North pole cap (red) const northCapGeometry = new THREE.ConeGeometry(0.04, 0.1, 8); const northCap = new THREE.Mesh(northCapGeometry, poleMaterial); northCap.position.set(0, 1.25, 0); northCap.rotation.x = Math.PI; // Point up axisHelper.add(northCap); // South pole cap (red) const southCapGeometry = new THREE.ConeGeometry(0.04, 0.1, 8); const southCap = new THREE.Mesh(southCapGeometry, poleMaterial); southCap.position.set(0, -1.25, 0); axisHelper.add(southCap); threejsScene.add(axisHelper); // Create renderer threejsRenderer = new THREE.WebGLRenderer({ antialias: true }); threejsRenderer.setSize(width, height); container.appendChild(threejsRenderer.domElement); // Add orbit controls threejsControls = new THREE.OrbitControls(threejsCamera, threejsRenderer.domElement); threejsControls.enableDamping = true; threejsControls.dampingFactor = 0.05; // Add ambient light const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); threejsScene.add(ambientLight); // Add directional light const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(1, 1, 1); threejsScene.add(directionalLight); // Start animation loop animate(); } // Initialize the 2D visualization with D3.js function init2DVisualization() { const container = document.getElementById('d3-container'); const width = container.clientWidth; const height = container.clientHeight; // Create SVG d3Svg = d3.select('#d3-container') .append('svg') .attr('width', width) .attr('height', height); // Add a background rectangle representing the ocean d3Svg.append('rect') .attr('width', width) .attr('height', height) .attr('fill', '#a4d1e9'); // Ocean blue // Create projection (Mercator) similar to standard world maps d3Projection = d3.geoMercator() .scale(width / (2 * Math.PI) * 0.9) .translate([width / 2, height / 1.8]); // Create path generator d3Path = d3.geoPath() .projection(d3Projection); // Draw graticule (grid lines) const graticule = d3.geoGraticule() .step([15, 15]); // Grid every 15 degrees d3Svg.append('path') .datum(graticule) .attr('class', 'graticule') .attr('d', d3Path) .style('fill', 'none') .style('stroke', '#ffffff') .style('stroke-width', '0.5px') .style('opacity', 0.5); // Draw equator const equator = { type: "LineString", coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]] }; d3Svg.append('path') .datum(equator) .attr('class', 'equator') .attr('d', d3Path) .style('fill', 'none') .style('stroke', '#00ffff') .style('stroke-width', '2px'); // Draw prime meridian (0° longitude) // const primeMeridian = { // type: "LineString", // coordinates: [[0, -90], [0, -45], [0, 0], [0, 45], [0, 90]] // }; // d3Svg.append('path') // .datum(primeMeridian) // .attr('class', 'prime-meridian') // .attr('d', d3Path) // .style('fill', 'none') // .style('stroke', '#ff0000') // .style('stroke-width', '2px'); // Draw continental outlines (simplified) // This just adds some landmass-like shapes to make it feel more Earth-like const mockContinents = [ { // North America (simplified) type: "Polygon", coordinates: [[ [-140, 70], [-120, 60], [-100, 50], [-80, 30], [-90, 20], [-100, 15], [-120, 30], [-130, 50], [-140, 70] ]] }, { // South America (simplified) type: "Polygon", coordinates: [[ [-80, 10], [-60, 0], [-50, -10], [-60, -30], [-70, -50], [-80, -30], [-90, -10], [-80, 10] ]] }, { // Europe/Africa (very simplified) type: "Polygon", coordinates: [[ [0, 60], [20, 40], [30, 30], [20, 10], [10, 0], [20, -10], [30, -30], [20, -40], [0, -30], [-10, 0], [0, 60] ]] }, { // Asia (simplified) type: "Polygon", coordinates: [[ [30, 60], [60, 70], [100, 60], [130, 40], [110, 20], [100, 0], [80, 10], [60, 30], [40, 40], [30, 60] ]] }, { // Australia (simplified) type: "Polygon", coordinates: [[ [110, -20], [130, -25], [140, -35], [130, -40], [120, -35], [110, -30], [110, -20] ]] } ]; // Add continent outlines with very low opacity to provide visual reference mockContinents.forEach(continent => { d3Svg.append('path') .datum(continent) .attr('class', 'continent-outline') .attr('d', d3Path); }); // Add labels for orientation const labels = [ { text: "North Pole", coords: [0, 85], anchor: "middle" }, { text: "South Pole", coords: [0, -85], anchor: "middle" }, { text: "Equator", coords: [-170, 0], anchor: "start" }, //{ text: "Prime Meridian", coords: [5, 45], anchor: "start" } ]; labels.forEach(label => { const [x, y] = d3Projection(label.coords); d3Svg.append('text') .attr('x', x) .attr('y', y) .attr('class', 'map-label') .attr('text-anchor', label.anchor) .text(label.text); }); } // Create a Three.js sphere from points function createThreeJSSphere(points) { // Remove existing sphere if any if (threejsSphere) { threejsScene.remove(threejsSphere); } // Create a group to hold the sphere and equator threejsSphere = new THREE.Group(); // Create geometry const geometry = new THREE.SphereGeometry(1, 32, 32); // Create material const material = new THREE.MeshPhongMaterial({ vertexColors: true, flatShading: true }); // Add equator const equatorGeometry = new THREE.TorusGeometry(1.01, 0.005, 16, 100); const equatorMaterial = new THREE.MeshBasicMaterial({ color: 0x00ffff }); const equator = new THREE.Mesh(equatorGeometry, equatorMaterial); equator.rotation.x = Math.PI / 2; // Rotate to lie on the x-z plane threejsSphere.add(equator); // Add colors to geometry const colors = []; const positionAttribute = geometry.getAttribute('position'); // For each vertex in the sphere for (let i = 0; i < positionAttribute.count; i++) { const vertex = new THREE.Vector3(); vertex.fromBufferAttribute(positionAttribute, i); vertex.normalize(); // Find closest point in our dataset let minDist = Infinity; let closestPoint = null; for (const point of points) { const pointVec = new THREE.Vector3(point.x, point.y, point.z); const dist = vertex.distanceTo(pointVec); if (dist < minDist) { minDist = dist; closestPoint = point; } } // Set color from closest point colors.push( closestPoint.color[0], closestPoint.color[1], closestPoint.color[2] ); } // Add colors to geometry geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); // Create mesh for the sphere const sphereMesh = new THREE.Mesh(geometry, material); // Add sphere to the group threejsSphere.add(sphereMesh); // Add group to scene threejsScene.add(threejsSphere); } // Create a D3.js map from points function createD3Map(points) { // Clear existing points but preserve graticule, equator, and axis d3Svg.selectAll('.planet-point').remove(); // Create groups for continents to improve organization const pointsGroup = d3Svg.append('g').attr('class', 'points-group'); // Plot each point on the map points.forEach(point => { // Convert spherical coordinates to longitude/latitude // phi goes from -π to π, convert to -180 to 180 degrees (longitude) const lon = (point.phi * 180 / Math.PI); // theta goes from 0 to π, convert to 90 to -90 degrees (latitude) const lat = 90 - (point.theta * 180 / Math.PI); // Skip points that might cause projection issues at the poles if (lat > 85 || lat < -85) return; // Only draw the point if it projects properly const projected = d3Projection([lon, lat]); if (projected) { pointsGroup.append('circle') .attr('class', 'planet-point') .attr('cx', projected[0]) .attr('cy', projected[1]) .attr('r', 1.5) .style('fill', `rgb(${point.color[0] * 255}, ${point.color[1] * 255}, ${point.color[2] * 255})`) .style('opacity', 0.8); } }); } // Animation loop for Three.js function animate() { requestAnimationFrame(animate); // Update controls if (threejsControls) { threejsControls.update(); } // Auto-rotate if enabled and on visualization screen if (document.getElementById('visualization-screen').classList.contains('active') && document.getElementById('auto-rotate').checked && threejsSphere) { threejsSphere.rotation.y += 0.005; } // Render scene if (threejsRenderer && threejsScene && threejsCamera) { threejsRenderer.render(threejsScene, threejsCamera); } } // Update visualizations with new data function updateVisualization() { if (planetData && planetData.points) { createThreeJSSphere(planetData.points); createD3Map(planetData.points); } } // Initialize visualizations function initVisualizations() { // Only initialize if not already done if (!threejsScene) { init3DVisualization(); } if (!d3Svg) { init2DVisualization(); } updateVisualization(); } // Start progress bar simulation function startProgressBar() { let progress = 0; const progressBar = document.getElementById('progress-bar'); const progressText = document.getElementById('progress-text'); clearInterval(progressInterval); progressBar.style.width = '0%'; progressText.textContent = '0%'; // Simulate progress updates progressInterval = setInterval(() => { if (progress >= 100) { clearInterval(progressInterval); return; } // Make progress increases less predictable const increment = Math.random() * 5 + 1; progress = Math.min(progress + increment, 100); progressBar.style.width = `${progress}%`; progressText.textContent = `${Math.round(progress)}%`; // When complete, show visualization if (progress >= 100) { setTimeout(() => { showScreen('visualization-screen'); }, 500); // Short delay for animation } }, 100); } // Generate planet function generatePlanet() { const pointsCount = parseInt(document.getElementById('points-count').value); const colorScheme = document.getElementById('color-scheme').value; // Show loading screen showScreen('loading-screen'); // Start progress bar startProgressBar(); // Call Python function to generate data with a slight delay // to allow the progress bar to start setTimeout(() => { pywebview.api.regenerate_planet(pointsCount, colorScheme) .then(data => { planetData = data; // Ensure we're at 100% when done document.getElementById('progress-bar').style.width = '100%'; document.getElementById('progress-text').textContent = '100%'; // Show visualization after a short delay setTimeout(() => { showScreen('visualization-screen'); }, 300); }) .catch(error => { console.error('Error generating planet:', error); alert('An error occurred while generating the planet. Please try again.'); showScreen('settings-screen'); }); }, 500); } // Set planet data and update visualizations function setPlanetData(data) { planetData = data; // Ensure progress is complete and move to visualization document.getElementById('progress-bar').style.width = '100%'; document.getElementById('progress-text').textContent = '100%'; // Clear any progress simulation clearInterval(progressInterval); // Show visualization screen after a short delay setTimeout(() => { showScreen('visualization-screen'); }, 300); } // Set up event listeners function setupEventListeners() { // Generate button on settings screen document.getElementById('generate-btn').addEventListener('click', generatePlanet); // Regenerate button on visualization screen document.getElementById('regenerate-btn').addEventListener('click', () => { const pointsCount = parseInt(document.getElementById('points-count').value); const colorScheme = document.getElementById('color-scheme').value; // Show loading screen showScreen('loading-screen'); // Start progress bar startProgressBar(); // Call Python function to regenerate data setTimeout(() => { pywebview.api.regenerate_planet(pointsCount, colorScheme) .then(data => { planetData = data; setTimeout(() => { showScreen('visualization-screen'); }, 300); }); }, 500); }); // Back button on visualization screen document.getElementById('back-btn').addEventListener('click', () => { showScreen('settings-screen'); }); // Window resize handler window.addEventListener('resize', handleResize); } // Initialize when document is ready document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); // Start on the settings screen showScreen('settings-screen'); // Start the animation loop anyway for when we need it animate(); });