From faaad5f09214b445447990f66df9b861d812b5c0 Mon Sep 17 00:00:00 2001 From: Tomi6545 <tomi6545@test> Date: Sun, 19 Jan 2025 19:25:12 +0100 Subject: [PATCH] update map + save --- public/ar_download.js | 73 +++ public/ar_main.js | 520 ++++++++++++++++++ public/ar_overviewmap.js | 177 ++++--- public/ar_start.js | 29 + public/index.html | 701 +------------------------ public/previewImages/download-icon.png | Bin 0 -> 7008 bytes public/styles.css | 207 ++++++++ 7 files changed, 953 insertions(+), 754 deletions(-) create mode 100644 public/ar_download.js create mode 100644 public/ar_main.js create mode 100644 public/ar_start.js create mode 100644 public/previewImages/download-icon.png create mode 100644 public/styles.css diff --git a/public/ar_download.js b/public/ar_download.js new file mode 100644 index 0000000..8ea47a6 --- /dev/null +++ b/public/ar_download.js @@ -0,0 +1,73 @@ +function downloadScene() { + const placedModels = getAllPlacedModels().map(model => { + const { lat, lng } = threeToLeaflet(model.position.x, model.position.z); + return { + name: model.modelConfig.name, + position: model.position, + rotation: model.rotation, + scale: model.scale, + lat: lat, + lng: lng, + } + }); + + console.log("Modelle: " + placedModels.length) + if (placedModels.length === 0) { + showInfoDialog("Szene ist leer"); + return; + } + + if (!geoLocation) { + console.log("Standort nicht geladen"); + showInfoDialog("Ihr Standort wurde noch nicht geladen"); + return; + } + + const data = { + lat: geoLocation.latitude, + lng: geoLocation.longitude, + models: placedModels, + } + + const sceneData = JSON.stringify(data, null, 1); + const blob = new Blob([sceneData], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "scene.json"; + a.style.display = "none"; + + document.body.appendChild(a); + a.click(); + + URL.revokeObjectURL(url); + document.body.removeChild(a); +} + +function loadSceneFromFile(loadedData) { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'application/json'; + + fileInput.addEventListener('change', function(event) { + const file = event.target.files[0]; + const fileName = file ? file.name : "Keine Datei ausgewählt" + if (!file) { + console.log("Keine Datei ausgewählt"); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const sceneData = JSON.parse(e.target.result); + loadedData(sceneData, fileName); + } catch (error) { + console.error("Fehler beim Verarbeiten der Datei:", error); + } + }; + + reader.readAsText(file); + }); + fileInput.click(); +} \ No newline at end of file diff --git a/public/ar_main.js b/public/ar_main.js new file mode 100644 index 0000000..51f0387 --- /dev/null +++ b/public/ar_main.js @@ -0,0 +1,520 @@ + // Variablen + let selectedModel = 'robot'; + let selectedPlacedModel = null; + let currentSession = null; + let reticle = null; + let scene, camera; + let geoLocation; + + let models = { + bench: { + name: "Bench", + image: "previewImages/bench.PNG", + file: "/assets/models/bench_model/scene.gltf", + scale: { x: 0.1, y: 0.1, z: 0.1 }, + minScale: 0.05, // 50% der aktuellen Größe + maxScale: 0.5 // 500% der aktuellen Größe + }, + trashbin: { + name: "Trash bin", + image: "previewImages/trash_can.PNG", + file: "/assets/models/trash_model/scene.gltf", + scale: { x: 0.03, y: 0.03, z: 0.03 }, + minScale: 0.01, // 50% der aktuellen Größe + maxScale: 0.1 // 500% der aktuellen Größe + }, + telephone_box: { + name: "Telephone Box", + image: "previewImages/telephone_box.PNG", + file: "/assets/models/telephone_box_model/scene.gltf", + scale: { x: 0.5, y: 0.5, z: 0.5 }, + minScale: 0.05, + maxScale: 1 + }, + fire_hydrant_model: { + name: "Fire Hydrant", + image: "previewImages/hydrant.PNG", + file: "/assets/models/fire_hydrant_model/scene.gltf", + scale: { x: 0.3, y: 0.3, z: 0.3 }, + minScale: 0.1, + maxScale: 1 + }, + statue: { + name: "Statue", + image: "previewImages/statue.PNG", + file: "/assets/models/statue_model/scene.gltf", + scale: { x: 0.5, y: 0.5, z: 0.5 }, + minScale: 0.05, + maxScale: 2 + }, + fountain: { + name: "Fountain", + image: "previewImages/fountain.PNG", + file: "/assets/models/fountain_model/scene.gltf", + scale: { x: 0.001, y: 0.001, z: 0.001 }, + minScale: 0.0005, + maxScale: 0.005 + }, + lantern: { + name: "Lantern", + image: "previewImages/lantern.jpg", + file: "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/Models/Lantern/glTF/Lantern.gltf", + scale: { x: 0.15, y: 0.15, z: 0.15 }, + minScale: 0.05, + maxScale: 0.3 + } + }; + + const menus = ['menu-bar', 'add-menu', 'edit-menu', 'options-menu', 'map-window']; + + window.onload = () => { + initializeAddMenu(); + + // Fügt Sound zu allen Buttons hinzu + const buttons = document.querySelectorAll("button, .menu-item"); + buttons.forEach(button => { + button.addEventListener("click", playButtonSound); + }); + }; + + function initializeAddMenu() { + const addMenu = document.getElementById('add-menu'); + addMenu.innerHTML = Object.entries(models) + .map( + ([key, model]) => ` + <div class="menu-item" id="${key}-item" onclick="selectModel('${key}')"> + <img src="${model.image}" alt="${model.name}" /> + </div> + ` + ) + .join('') + + ` + <div class="menu-item" onclick="showMenu('menu-bar')"> + <img src="previewImages/back-icon.png" alt="Zurück" /> + </div> + `; + } + + function showMenu(menuId) { + const isMapWindow = menuId === 'map-window'; + if (isMapWindow && !geoLocation) { + console.log("Standort nicht geladen"); + showInfoDialog("Ihr Standort wurde noch nicht geladen"); + return; + } + + menus.forEach(id => { + document.getElementById(id).style.display = id === menuId ? 'flex' : 'none'; + }); + closeDynamicMenu(); + if (menuId === 'menu-bar') clearSelectedModel(); + else if (isMapWindow) init_map(); + } + + function clearSelectedModel() { + if (selectedPlacedModel) { + selectedPlacedModel.traverse((child) => { + if (child.isMesh) { + child.material.emissive.setHex(0x000000); // Markierung entfernen + } + }); + selectedPlacedModel = null; // Kein Modell mehr ausgewählt + } + } + + function selectModel(modelId) { + const model = models[modelId]; + if (model && model.file) { + loadModel(model.file); + showMenu('menu-bar'); + } + } + + function loadModel(filePath) { + placeModel(filePath, reticle.position, (model) => { + selectedPlacedModel = model; + highlightSelectedModel(); + showMenu('edit-menu'); + }); + } + + function placeModel(filePath, position, onPlace = null) { + const modelConfig = Object.values(models).find(model => model.file === filePath); + const loader = new THREE.GLTFLoader(); + loader.load( + filePath, + (gltf) => { + const model = gltf.scene; + if (modelConfig && modelConfig.scale) { + model.scale.set(modelConfig.scale.x, modelConfig.scale.y, modelConfig.scale.z); + } + model.position.copy(position); + model.isPlacedModel = true; + model.modelConfig = modelConfig; + scene.add(model); + + if (onPlace) { + onPlace(model); + } + }, + undefined, + (error) => { + console.error("Fehler beim Laden des Modells:", error); + } + ); + } + + function highlightSelectedModel() { + if (selectedPlacedModel) { + removeHighlightFromAllModels(); + selectedPlacedModel.traverse((child) => { + if (child.isMesh) { + child.material.emissive.setHex(0xff0000); // Rote Hervorhebung + } + }); + } + } + + function removeHighlightFromAllModels() { + scene.traverse((child) => { + if (child.isMesh && child.material && child.material.emissive) { + child.material.emissive.setHex(0x000000); // Markierung entfernen + } + }); + } + + function removeHighlightFromSelectedModel() { + if (selectedPlacedModel) { + selectedPlacedModel.traverse((child) => { + if (child.isMesh) child.material.emissive.setHex(0x000000); // Markierung entfernen + }); + } + } + + function selectModelFromScene(event) { + const mouse = new THREE.Vector2( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1 + ); + + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(mouse, camera); + + // Prüfe Kollisionen mit Objekten in der Szene + const intersects = raycaster.intersectObjects(scene.children, true); + + if (intersects.length > 0) { + // Finde das Hauptobjekt (Root-Parent), falls Mesh ausgewählt wurde + let selectedObject = intersects[0].object; + while (selectedObject.parent && selectedObject.parent !== scene) { + selectedObject = selectedObject.parent; + } + + // Markiere das gesamte Modell als ausgewählt + selectedPlacedModel = selectedObject; + highlightSelectedModel(); + showMenu("edit-menu"); + } + } + + function completeEditing() { + removeHighlightFromSelectedModel(); + closeDynamicMenu(); + selectedPlacedModel = null; + document.getElementById('edit-menu').style.display = 'none'; + document.getElementById('menu-bar').style.display = 'flex'; + } + + function openRotationMenu() { + if (!selectedPlacedModel) { + showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); + return; + } + + // Holen Sie die aktuelle Y-Rotation des Modells (in Grad) + const currentRotation = Math.round(THREE.MathUtils.radToDeg(selectedPlacedModel.rotation.y)); + + const dynamicMenu = document.getElementById("dynamic-menu"); + dynamicMenu.style.display = "flex"; + dynamicMenu.innerHTML = ` + <h3>Rotation anpassen</h3> + <label>Y-Achse: <span id="current-rotation">${currentRotation}</span>°<input type="range" min="0" max="360" step="10" onchange="updateRotation('y', this.value)"></label> + <button onclick="closeDynamicMenu()">Zurück</button> + `; + } + + function updateRotation(axis, value) { + if (selectedPlacedModel) { + const radians = (value / 180) * Math.PI; + selectedPlacedModel.rotation[axis] = radians; + + // Update der aktuellen Rotation im Menü + const currentRotationDisplay = document.getElementById("current-rotation"); + if (currentRotationDisplay) { + currentRotationDisplay.textContent = value; // Zeige den aktuellen Wert in Grad an + } + } + } + + function calculateBoundingBox(object) { + const box = new THREE.Box3().setFromObject(object); + const size = new THREE.Vector3(); + box.getSize(size); + return size; + } + + + function calculateMaxScale(object) { + const boundingBox = calculateBoundingBox(object); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Berechne die maximal mögliche Skalierung, um im Viewport zu bleiben + const scaleWidth = viewportWidth / boundingBox.x; + const scaleHeight = viewportHeight / boundingBox.y; + + // Wähle den kleineren Wert und reduziere ihn leicht, um sicherzugehen, dass das Objekt nicht über den Rand hinausgeht + const safeScaleFactor = 0.95; // Puffer, um sicherzustellen, dass es nicht zu groß wird + + return Math.min(scaleWidth, scaleHeight) * safeScaleFactor; + } + + + /** + function openScaleMenu() { + if (!selectedPlacedModel) { + showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); + return; + } + + // Berechne die maximale Skalierung für das spezifische Objekt + const maxScale = calculateMaxScale(selectedPlacedModel); + + // Aktuelle Skalierung des Modells bestimmen + const currentScale = selectedPlacedModel.scale.x; + + const dynamicMenu = document.getElementById("dynamic-menu"); + dynamicMenu.style.display = "flex"; + dynamicMenu.innerHTML = ` + <h3>Skalierung anpassen</h3> + <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span><input type="range" min="0.01" max="${maxScale.toFixed(2)}" step="0.0001" value="${currentScale}" onchange="updateScale(this.value)"></label> + <button onclick="closeDynamicMenu()">Zurück</button> + `; + } * */ + + + + + + function openScaleMenu() { + if (!selectedPlacedModel) { + showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); + return; + } + + // Berechne die maximale Skalierung für das spezifische Objekt + // const maxScale = calculateMaxScale(selectedPlacedModel); + + // Aktuelle Skalierung des Modells bestimmen + const currentScale = selectedPlacedModel.scale.x; + const minScale = selectedPlacedModel.modelConfig.minScale; + const maxScale = selectedPlacedModel.modelConfig.maxScale; + const step = (maxScale - minScale) / 100; // Dynamische Schrittgröße basierend auf Grenzen + + console.log("Slider-Werte:", { minScale, maxScale, currentScale }); + + const dynamicMenu = document.getElementById("dynamic-menu"); + dynamicMenu.style.display = "flex"; + dynamicMenu.innerHTML = ` + <h3>Skalierung anpassen</h3> + <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span> + <input type="range" min="${minScale}" max="${maxScale}" step="${step}" value="${currentScale}" onchange="updateScale(this.value)"></label> + <button onclick="closeDynamicMenu()">Zurück</button> + `; + } + + function updateScale(value) { + if (selectedPlacedModel) { + const scale = parseFloat(value); + selectedPlacedModel.scale.set(scale, scale, scale); + + // Anzeige des aktuellen Wertes aktualisieren + const scaleValueDisplay = document.getElementById("scale-value"); + if (scaleValueDisplay) { + scaleValueDisplay.textContent = `${scale.toFixed(2)}`; + } + } + } + + function deleteModel() { + if (!selectedPlacedModel) { + showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es löschen."); + return; + } + + // Dialog anzeigen + const deleteDialog = document.getElementById('delete-confirmation-dialog'); + deleteDialog.style.display = 'flex'; + } + + function confirmDelete(shouldDelete) { + const deleteDialog = document.getElementById('delete-confirmation-dialog'); + deleteDialog.style.display = 'none'; + + if (shouldDelete && selectedPlacedModel) { + scene.remove(selectedPlacedModel); + selectedPlacedModel = null; + showMenu('menu-bar'); + } + } + + function closeDynamicMenu() { + const dynamicMenu = document.getElementById("dynamic-menu"); + dynamicMenu.style.display = "none"; + } + + function getAllPlacedModels() { + return scene.children.filter(child => child.isPlacedModel === true); + } + + function refreshMapDialog() { + const mapDialog = document.getElementById('map-dialog'); + mapDialog.style.display = 'flex'; + } + + function closeMapDialog() { + const mapDialog = document.getElementById('map-dialog'); + mapDialog.style.display = 'none'; + } + + async function activateXR(sceneData = null) { + const canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + const gl = canvas.getContext('webgl', { xrCompatible: true }); + const renderer = new THREE.WebGLRenderer({ alpha: true, canvas, context: gl }); + renderer.autoClear = false; + + scene = new THREE.Scene(); + camera = new THREE.PerspectiveCamera(); + camera.matrixAutoUpdate = false; + + const light = new THREE.DirectionalLight(0xffffff, 1); + light.position.set(10, 10, 10); + scene.add(light); + + const loader = new THREE.GLTFLoader(); + loader.load("https://immersive-web.github.io/webxr-samples/media/gltf/reticle/reticle.gltf", (gltf) => { + reticle = gltf.scene; + reticle.visible = false; + scene.add(reticle); + }); + + currentSession = await navigator.xr.requestSession('immersive-ar', { + optionalFeatures: ["dom-overlay"], + domOverlay: { root: document.body }, + requiredFeatures: ['hit-test'] + }); + + currentSession.updateRenderState({ baseLayer: new XRWebGLLayer(currentSession, gl) }); + const referenceSpace = await currentSession.requestReferenceSpace('local'); + const viewerSpace = await currentSession.requestReferenceSpace('viewer'); + const hitTestSource = await currentSession.requestHitTestSource({ space: viewerSpace }); + + document.getElementById('menu-bar').style.display = 'flex'; + + currentSession.addEventListener("end", () => { + currentSession = null; + menus.forEach(id => { + document.getElementById(id).style.display = 'none'; + }); + }); + + canvas.addEventListener("pointerdown", selectModelFromScene); + + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(position => { + geoLocation = { + latitude: roundTo(position.coords.latitude, 5), + longitude: roundTo(position.coords.longitude, 5), + }; + console.log("GeoLocation: " + JSON.stringify(geoLocation)); + if (sceneData) { + sceneData.models.forEach((model) => { + const offSet = leafletToThree(sceneData.lat, sceneData.lng) + if (model.name) { + const filePath = Object.values(models).find(m => model.name === m.name).file; + const { x, z } = leafletToThree(model.lat, model.lng); + const positionVector = new THREE.Vector3(x, model.position.y, z); + placeModel(filePath, positionVector, (placed) => { + if (model.rotation) { + placed.rotation._x = model.rotation._x; + placed.rotation._y = model.rotation._y; + placed.rotation._z = model.rotation._z; + } + if (model.scale) { + placed.scale.x = model.scale.x; + placed.scale.y = model.scale.y; + placed.scale.z = model.scale.z; + } + }); + } + }); + } + }, function(error) { + console.error("Fehler bei der Geolokalisierung:", JSON.stringify(error)); + }, {enableHighAccuracy: true, maximumAge: 2000, timeout: 5000}); + } + + currentSession.requestAnimationFrame(function onXRFrame(time, frame) { + currentSession.requestAnimationFrame(onXRFrame); + gl.bindFramebuffer(gl.FRAMEBUFFER, currentSession.renderState.baseLayer.framebuffer); + + const pose = frame.getViewerPose(referenceSpace); + if (pose) { + const view = pose.views[0]; + const viewport = currentSession.renderState.baseLayer.getViewport(view); + renderer.setSize(viewport.width, viewport.height); + + camera.matrix.fromArray(view.transform.matrix); + camera.projectionMatrix.fromArray(view.projectionMatrix); + camera.updateMatrixWorld(true); + + const hitTestResults = frame.getHitTestResults(hitTestSource); + if (hitTestResults.length > 0) { + const hitPose = hitTestResults[0].getPose(referenceSpace); + reticle.visible = true; + reticle.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z); + reticle.updateMatrixWorld(true); + } + + renderer.render(scene, camera); + } + }); + } + + function exitAR() { + document.getElementById('confirmation-dialog').style.display = 'flex'; + } + + function confirmExit(shouldExit) { + if (shouldExit && currentSession) { + currentSession.end(); + } + document.getElementById('confirmation-dialog').style.display = 'none'; + } + + let soundTimeout = false; + + function playButtonSound() { + if (!soundTimeout) { + const sound = document.getElementById("button-sound"); + sound.currentTime = 0; + sound.play(); + + soundTimeout = true; + setTimeout(() => { + soundTimeout = false; + }, 200); // Verzögerung von 200ms + } + } \ No newline at end of file diff --git a/public/ar_overviewmap.js b/public/ar_overviewmap.js index 2db762e..c7e0e47 100644 --- a/public/ar_overviewmap.js +++ b/public/ar_overviewmap.js @@ -1,70 +1,127 @@ - - +let init = false; +let mymap; +let userIcon; +let intervalId; +let modeMarkers = [] +const earthRadius = 6378137; // Erdradius in Metern (WGS84) function init_map() { - const mapContainer = document.getElementById('map-container'); + const mapContainer = document.getElementById("map-container"); if (mapContainer) { - var mymap = L.map(mapContainer); - var OpenStreetMap_DE = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { - maxZoom: 30, - minZoom: 15, - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' - }); - // select one basemap from https://leaflet-extras.github.io/leaflet-providers/preview/ - var CartoDB_Positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', - subdomains: 'abcd', - maxZoom: 30, - minZoom: 15 + if (!init) { + mymap = L.map(mapContainer); + var OpenStreetMap_DE = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { + maxZoom: 30, + minZoom: 15, + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + }); + // select one basemap from https://leaflet-extras.github.io/leaflet-providers/preview/ + var CartoDB_Positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', + subdomains: 'abcd', + maxZoom: 30, + minZoom: 15 + }); + CartoDB_Positron.addTo(mymap); + + userIcon = L.icon({ + iconUrl: 'previewImages/map-user.png', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + }); + + mymap.setView([geoLocation.latitude, geoLocation.longitude], 20) + userIcon = L.marker([geoLocation.latitude, geoLocation.longitude], { icon: userIcon }).bindPopup('Ihre Position').addTo(mymap); + init = true; + } + + modeMarkers.forEach(marker => { + marker.remove(); }); - CartoDB_Positron.addTo(mymap); - - // Marker initialisieren - var userIcon = L.icon({ - iconUrl: 'previewImages/map-user.png', // Dein Männchen-Bild - iconSize: [32, 32], // Die Größe des Icons - iconAnchor: [16, 32], // Der Punkt des Markers, der auf der Position zeigt - popupAnchor: [0, -32] // Die Position des Popups relativ zum Marker + const models = getAllPlacedModels(); + models.forEach(model => { + const { lat, lng } = threeToLeaflet(model.position.x, model.position.z); + const modelMarker = L.marker([lat, lng], { icon: L.icon({ + iconUrl: model.modelConfig.image, + iconSize: [16, 16], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + })}).addTo(mymap).bindPopup(model.modelConfig?.name || "Unbenannt"); + modeMarkers.push(modelMarker); }); + intervalId = setInterval(updateMap, 500); + } + } - // Marker initialisieren - var userMarker = L.marker([0, 0], { icon: userIcon }).bindPopup('Ihre Position').addTo(mymap); - let initPosition = false; - let lastPosition = { lat: 0, lng: 0 }; // Letzte bekannte Position - const updateThreshold = 1.5; // Schwellenwert für den Abstand (in Metern) - - // Funktion, um die Position zu aktualisieren - function updatePosition(position) { - const userLat = position.coords.latitude; - const userLng = position.coords.longitude; - - const currentPosition = L.latLng(userLat, userLng); - const lastKnownPosition = L.latLng(lastPosition.lat, lastPosition.lng); - const distance = currentPosition.distanceTo(lastKnownPosition); - - // Nur aktualisieren, wenn der Abstand größer als der Schwellenwert ist - if (distance > updateThreshold || !initPosition) { - if (!initPosition) { - mymap.setView([userLat, userLng], 20); // Zentrische Ansicht beim ersten Mal - initPosition = true; - } - - // Marker auf die neue Position setzen - userMarker.setLatLng([userLat, userLng]); - console.log(`Setze Position auf lat=${userLat} lng=${userLng}, Abstand: ${distance}m`); - lastPosition = { lat: userLat, lng: userLng }; - } - } + function updateMap() { + if (document.getElementById("map-window").style.display === 'none' && intervalId) { + clearInterval(intervalId); + intervalId = null; + return; + } - // Die Position alle 5 Sekunden aktualisieren - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(updatePosition); - navigator.geolocation.watchPosition(updatePosition, function(error) { - console.error("Fehler bei der Geolokalisierung:", error); - }, {enableHighAccuracy: true, maximumAge: 2000,timeout: 5000}); - } else { - console.error("Geolocation wird nicht unterstützt."); - } + let vec = new THREE.Vector3(); + const userPosition = camera.getWorldPosition(vec); + const { lat, lng } = threeToLeaflet(userPosition.x, userPosition.z); + + if (userIcon) { + userIcon.setLatLng([lat, lng]); } } + + + + function threeToLeaflet(threeX, threeZ) { + if (!geoLocation) { + console.error("GeoLocation ist noch nicht verfügbar"); + return; + } + + const originLat = geoLocation.latitude; + const originLng = geoLocation.longitude; + + const deltaLat = (threeZ / earthRadius) * (180 / Math.PI); + const deltaLng = (threeX / (earthRadius * Math.cos(Math.PI * originLat / 180))) * (180 / Math.PI); + + const globalLat = originLat + deltaLat; + const globalLng = originLng + deltaLng; + return { lat: globalLat, lng: globalLng }; +} + +function leafletToThree(lat, lng) { + if (!geoLocation) { + console.error("GeoLocation ist noch nicht verfügbar"); + return; + } + + const originLat = geoLocation.latitude; + const originLng = geoLocation.longitude; + + const deltaLat = lat - originLat; + const deltaLng = lng - originLng; + + const threeZ = deltaLat * (Math.PI / 180) * earthRadius; + const threeX = deltaLng * (Math.PI / 180) * earthRadius * Math.cos(Math.PI * originLat / 180); + + return { x: threeX, z: threeZ }; +} + +function roundTo(n, digits) { + var negative = false; + if (digits === undefined) { + digits = 0; + } + if (n < 0) { + negative = true; + n = n * -1; + } + var multiplicator = Math.pow(10, digits); + n = parseFloat((n * multiplicator).toFixed(11)); + n = (Math.round(n) / multiplicator).toFixed(digits); + if (negative) { + n = (n * -1).toFixed(digits); + } + return parseFloat(n); +} \ No newline at end of file diff --git a/public/ar_start.js b/public/ar_start.js new file mode 100644 index 0000000..f537a70 --- /dev/null +++ b/public/ar_start.js @@ -0,0 +1,29 @@ +if (navigator.xr) { + let sceneData; + const startButton = document.createElement('button'); + startButton.textContent = 'Start AR'; + startButton.style.cssText = "position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; + document.body.appendChild(startButton); + startButton.onclick = () => { + startButton.remove(); + loadButton.remove(); + activateXR(sceneData); + }; + + const loadButton = document.createElement('button'); + loadButton.textContent = 'Umgebung Laden'; + loadButton.style.cssText = "position: fixed; top: 55%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; + document.body.appendChild(loadButton); + loadButton.onclick = () => { + loadSceneFromFile((data, fileName) => { + if (data && fileName) { + loadButton.textContent = fileName; + sceneData = data + } else { + loadButton.textContent = 'Umgebung Laden'; + } + }); + }; + } else { + alert('WebXR wird nicht unterstützt.'); + } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 8686486..f297f1b 100644 --- a/public/index.html +++ b/public/index.html @@ -5,215 +5,7 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>GeoVis AR Projekt</title> - <style> - body { - margin: 0; - font-family: Arial, sans-serif; - background-color: #f0f0f0; - color: #333; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-image: url('https://www.hft-stuttgart.de/fileadmin/Dateien/Hochschule/-_R_Juergen_Pollak_HFT_18.04.18-0091.jpg'); - background-size: cover; - background-position: center; - } - - .container { - text-align: center; - background-color: rgba(255, 255, 255, 0.9); - padding: 50px; - border-radius: 10px; - color: #333; - max-width: 80%; - box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2); - } - - h1 { - font-size: 3em; - margin-bottom: 20px; - color: #444; - } - - p { - font-size: 1.2em; - margin-bottom: 30px; - } - - #add-menu { - overflow-y: auto; - - } - - #menu-bar, - .menu-placeholder { - position: absolute; - bottom: 0; - width: 100%; - height: 80px; - display: flex; - justify-content: space-around; - align-items: center; - background: #121212; - padding: 10px; - box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.8); - color: white; - z-index: 10; - border-top: 1px solid #2a2a2a; - } - - .menu-item { - background: #2c2c2c; - border-radius: 20px; - padding: 8px; - transition: background-color 0.3s, transform 0.2s; - } - - .menu-item:hover { - background-color: #3a3a3a; - transform: scale(1.1); - } - - .menu-item img { - width: 50px; - height: 50px; - } - - .menu-placeholder .menu-item img { - width: 50px; - height: 50px; - } - - button { - background-color: #4CAF50; - color: white; - font-size: 1em; - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease-in-out, transform 0.2s; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 10px; - } - - button:hover { - background-color: #45a049; - } - - button:active { - background-color: #387a39; - } - - /* Confirmation Dialog */ - #confirmation-dialog, - #delete-confirmation-dialog, - #info-dialog { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - z-index: 20; - } - - .dialog-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - } - - .dialog-box { - position: relative; - background: #2c2c2c; - padding: 20px; - border-radius: 10px; - text-align: center; - color: white; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8); - } - - .dialog-box p { - margin-bottom: 20px; - font-size: 1.2em; - color: #f0f0f0; - } - - .dialog-box button { - margin: 5px; - padding: 10px 20px; - font-size: 16px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease, transform 0.2s; - } - - .dialog-box button:first-child { - background-color: #e74c3c; - color: white; - } - - .dialog-box button:first-child:hover { - background-color: #c0392b; - transform: scale(1.05); - } - - .dialog-box button:last-child { - background-color: #3498db; - color: white; - } - - .dialog-box button:last-child:hover { - background-color: #2980b9; - transform: scale(1.05); - } - - - #dynamic-menu { - position: absolute; - bottom: 80px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; - background: #1e1e1e; - box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.8); - color: white; - width: 100vw; - height: 200px; - z-index: 20; - overflow-y: auto; - border-radius: 8px; - } - - - #dynamic-menu input[type="range"] { - width: 100%; - margin: 10px auto; - background: #3a3a3a; - border-radius: 5px; - } - - #dynamic-menu input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - background: #fff; - border: 2px solid #555; - border-radius: 50%; - cursor: pointer; - } - </style> + <link rel="stylesheet" href="styles.css"> <script src="https://unpkg.com/three@0.126.0/build/three.js"></script> <script src="https://unpkg.com/three@0.126.0/examples/js/loaders/GLTFLoader.js"></script> </head> @@ -289,6 +81,9 @@ <div class="menu-item" onclick="exitAR()"> <img src="previewImages/exit-icon.png" alt="Beenden" /> </div> + <div class="menu-item" onclick="downloadScene()"> + <img src="previewImages/download-icon.png" alt="Download" /> + </div> <div class="menu-item" onclick="showMenu('menu-bar')"> <img src="previewImages/back-icon.png" alt="Zurück" /> </div> @@ -336,498 +131,16 @@ </div> - + <script src="ar_start.js"></script> + <script src="ar_main.js"></script> <script src="ar_debug_console.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <!-- Leaflet einbinden --> <script src="ar_overviewmap.js"></script> + <script src="ar_download.js"></script> <!-- Audio-Element für Button-Klick --> <audio id="button-sound" src="sounds/button-sound.mp3" preload="auto"></audio> - - <script> - // Variablen - let selectedModel = 'robot'; - let selectedPlacedModel = null; - let currentSession = null; - let reticle = null; - let scene, camera; - - let models = { - bench: { - name: "Bench", - image: "previewImages/bench.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/bench_model/scene.gltf", - scale: { x: 0.1, y: 0.1, z: 0.1 }, - minScale: 0.05, // 50% der aktuellen Größe - maxScale: 0.5 // 500% der aktuellen Größe - }, - trashbin: { - name: "Trash bin", - image: "previewImages/trash_can.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/trash_model/scene.gltf", - scale: { x: 0.03, y: 0.03, z: 0.03 }, - minScale: 0.01, // 50% der aktuellen Größe - maxScale: 0.1 // 500% der aktuellen Größe - }, - telephone_box: { - name: "Telephone Box", - image: "previewImages/telephone_box.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/telephone_box_model/scene.gltf", - scale: { x: 0.5, y: 0.5, z: 0.5 }, - minScale: 0.05, - maxScale: 1 - }, - fire_hydrant_model: { - name: "Fire Hydrant", - image: "previewImages/hydrant.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/fire_hydrant_model/scene.gltf", - scale: { x: 0.3, y: 0.3, z: 0.3 }, - minScale: 0.1, - maxScale: 1 - }, - statue: { - name: "Statue", - image: "previewImages/statue.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/statue_model/scene.gltf", - scale: { x: 0.5, y: 0.5, z: 0.5 }, - minScale: 0.05, - maxScale: 2 - }, - fountain: { - name: "Fountain", - image: "previewImages/fountain.PNG", - file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/fountain_model/scene.gltf", - scale: { x: 0.001, y: 0.001, z: 0.001 }, - minScale: 0.0005, - maxScale: 0.005 - }, - lantern: { - name: "Lantern", - image: "previewImages/lantern.jpg", - file: "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/Models/Lantern/glTF/Lantern.gltf", - scale: { x: 0.15, y: 0.15, z: 0.15 }, - minScale: 0.05, - maxScale: 0.3 - } - }; - - const menus = ['menu-bar', 'add-menu', 'edit-menu', 'options-menu', 'map-window']; - - window.onload = () => { - initializeAddMenu(); - - // Fügt Sound zu allen Buttons hinzu - const buttons = document.querySelectorAll("button, .menu-item"); - buttons.forEach(button => { - button.addEventListener("click", playButtonSound); - }); - }; - - function initializeAddMenu() { - const addMenu = document.getElementById('add-menu'); - addMenu.innerHTML = Object.entries(models) - .map( - ([key, model]) => ` - <div class="menu-item" id="${key}-item" onclick="selectModel('${key}')"> - <img src="${model.image}" alt="${model.name}" /> - </div> - ` - ) - .join('') + - ` - <div class="menu-item" onclick="showMenu('menu-bar')"> - <img src="previewImages/back-icon.png" alt="Zurück" /> - </div> - `; - } - - function showMenu(menuId) { - menus.forEach(id => { - document.getElementById(id).style.display = id === menuId ? 'flex' : 'none'; - }); - closeDynamicMenu(); - if (menuId === 'menu-bar') clearSelectedModel(); - else if (menuId === 'map-window') init_map(); - } - - function clearSelectedModel() { - if (selectedPlacedModel) { - selectedPlacedModel.traverse((child) => { - if (child.isMesh) { - child.material.emissive.setHex(0x000000); // Markierung entfernen - } - }); - selectedPlacedModel = null; // Kein Modell mehr ausgewählt - } - } - - function selectModel(modelId) { - const model = models[modelId]; - if (model && model.file) { - loadModel(model.file); - showMenu('menu-bar'); - } - } - - function loadModel(filePath) { - const modelConfig = Object.values(models).find(model => model.file === filePath); - const loader = new THREE.GLTFLoader(); - loader.load( - filePath, - (gltf) => { - const model = gltf.scene; - if (modelConfig && modelConfig.scale) { - model.scale.set(modelConfig.scale.x, modelConfig.scale.y, modelConfig.scale.z); - } - model.position.copy(reticle.position); - scene.add(model); - - // Speichere das platzierte Modell und die Konfiguration - selectedPlacedModel = model; - selectedPlacedModel.modelConfig = modelConfig; - - highlightSelectedModel(); - showMenu('edit-menu'); - }, - undefined, - (error) => { - console.error("Fehler beim Laden des Modells:", error); - } - ); - } - - function highlightSelectedModel() { - if (selectedPlacedModel) { - removeHighlightFromAllModels(); - selectedPlacedModel.traverse((child) => { - if (child.isMesh) { - child.material.emissive.setHex(0xff0000); // Rote Hervorhebung - } - }); - } - } - - function removeHighlightFromAllModels() { - scene.traverse((child) => { - if (child.isMesh && child.material && child.material.emissive) { - child.material.emissive.setHex(0x000000); // Markierung entfernen - } - }); - } - - function removeHighlightFromSelectedModel() { - if (selectedPlacedModel) { - selectedPlacedModel.traverse((child) => { - if (child.isMesh) child.material.emissive.setHex(0x000000); // Markierung entfernen - }); - } - } - - function selectModelFromScene(event) { - const mouse = new THREE.Vector2( - (event.clientX / window.innerWidth) * 2 - 1, - -(event.clientY / window.innerHeight) * 2 + 1 - ); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(mouse, camera); - - // Prüfe Kollisionen mit Objekten in der Szene - const intersects = raycaster.intersectObjects(scene.children, true); - - if (intersects.length > 0) { - // Finde das Hauptobjekt (Root-Parent), falls Mesh ausgewählt wurde - let selectedObject = intersects[0].object; - while (selectedObject.parent && selectedObject.parent !== scene) { - selectedObject = selectedObject.parent; - } - - // Markiere das gesamte Modell als ausgewählt - selectedPlacedModel = selectedObject; - highlightSelectedModel(); - showMenu("edit-menu"); - } - } - - function completeEditing() { - removeHighlightFromSelectedModel(); - closeDynamicMenu(); - selectedPlacedModel = null; - document.getElementById('edit-menu').style.display = 'none'; - document.getElementById('menu-bar').style.display = 'flex'; - } - - function openRotationMenu() { - if (!selectedPlacedModel) { - showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); - return; - } - - // Holen Sie die aktuelle Y-Rotation des Modells (in Grad) - const currentRotation = Math.round(THREE.MathUtils.radToDeg(selectedPlacedModel.rotation.y)); - - const dynamicMenu = document.getElementById("dynamic-menu"); - dynamicMenu.style.display = "flex"; - dynamicMenu.innerHTML = ` - <h3>Rotation anpassen</h3> - <label>Y-Achse: <span id="current-rotation">${currentRotation}</span>°<input type="range" min="0" max="360" step="10" onchange="updateRotation('y', this.value)"></label> - <button onclick="closeDynamicMenu()">Zurück</button> - `; - } - - function updateRotation(axis, value) { - if (selectedPlacedModel) { - const radians = (value / 180) * Math.PI; - selectedPlacedModel.rotation[axis] = radians; - - // Update der aktuellen Rotation im Menü - const currentRotationDisplay = document.getElementById("current-rotation"); - if (currentRotationDisplay) { - currentRotationDisplay.textContent = value; // Zeige den aktuellen Wert in Grad an - } - } - } - - function calculateBoundingBox(object) { - const box = new THREE.Box3().setFromObject(object); - const size = new THREE.Vector3(); - box.getSize(size); - return size; - } - - - function calculateMaxScale(object) { - const boundingBox = calculateBoundingBox(object); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Berechne die maximal mögliche Skalierung, um im Viewport zu bleiben - const scaleWidth = viewportWidth / boundingBox.x; - const scaleHeight = viewportHeight / boundingBox.y; - - // Wähle den kleineren Wert und reduziere ihn leicht, um sicherzugehen, dass das Objekt nicht über den Rand hinausgeht - const safeScaleFactor = 0.95; // Puffer, um sicherzustellen, dass es nicht zu groß wird - - return Math.min(scaleWidth, scaleHeight) * safeScaleFactor; - } - - - /** - function openScaleMenu() { - if (!selectedPlacedModel) { - showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); - return; - } - - // Berechne die maximale Skalierung für das spezifische Objekt - const maxScale = calculateMaxScale(selectedPlacedModel); - - // Aktuelle Skalierung des Modells bestimmen - const currentScale = selectedPlacedModel.scale.x; - - const dynamicMenu = document.getElementById("dynamic-menu"); - dynamicMenu.style.display = "flex"; - dynamicMenu.innerHTML = ` - <h3>Skalierung anpassen</h3> - <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span><input type="range" min="0.01" max="${maxScale.toFixed(2)}" step="0.0001" value="${currentScale}" onchange="updateScale(this.value)"></label> - <button onclick="closeDynamicMenu()">Zurück</button> - `; - } * */ - - - - - - function openScaleMenu() { - if (!selectedPlacedModel) { - showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); - return; - } - - // Berechne die maximale Skalierung für das spezifische Objekt - // const maxScale = calculateMaxScale(selectedPlacedModel); - - // Aktuelle Skalierung des Modells bestimmen - const currentScale = selectedPlacedModel.scale.x; - const minScale = selectedPlacedModel.modelConfig.minScale; - const maxScale = selectedPlacedModel.modelConfig.maxScale; - const step = (maxScale - minScale) / 100; // Dynamische Schrittgröße basierend auf Grenzen - - console.log("Slider-Werte:", { minScale, maxScale, currentScale }); - - const dynamicMenu = document.getElementById("dynamic-menu"); - dynamicMenu.style.display = "flex"; - dynamicMenu.innerHTML = ` - <h3>Skalierung anpassen</h3> - <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span> - <input type="range" min="${minScale}" max="${maxScale}" step="${step}" value="${currentScale}" onchange="updateScale(this.value)"></label> - <button onclick="closeDynamicMenu()">Zurück</button> - `; - } - - function updateScale(value) { - if (selectedPlacedModel) { - const scale = parseFloat(value); - selectedPlacedModel.scale.set(scale, scale, scale); - - // Anzeige des aktuellen Wertes aktualisieren - const scaleValueDisplay = document.getElementById("scale-value"); - if (scaleValueDisplay) { - scaleValueDisplay.textContent = `${scale.toFixed(2)}`; - } - } - } - - function deleteModel() { - if (!selectedPlacedModel) { - showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es löschen."); - return; - } - - // Dialog anzeigen - const deleteDialog = document.getElementById('delete-confirmation-dialog'); - deleteDialog.style.display = 'flex'; - } - - function confirmDelete(shouldDelete) { - const deleteDialog = document.getElementById('delete-confirmation-dialog'); - deleteDialog.style.display = 'none'; - - if (shouldDelete && selectedPlacedModel) { - scene.remove(selectedPlacedModel); - selectedPlacedModel = null; - showMenu('menu-bar'); - } - } - - function closeDynamicMenu() { - const dynamicMenu = document.getElementById("dynamic-menu"); - dynamicMenu.style.display = "none"; - } - - function refreshMapDialog() { - const mapDialog = document.getElementById('map-dialog'); - mapDialog.style.display = 'flex'; - } - - function closeMapDialog() { - const mapDialog = document.getElementById('map-dialog'); - mapDialog.style.display = 'none'; - } - - async function activateXR() { - const canvas = document.createElement('canvas'); - document.body.appendChild(canvas); - const gl = canvas.getContext('webgl', { xrCompatible: true }); - const renderer = new THREE.WebGLRenderer({ alpha: true, canvas, context: gl }); - renderer.autoClear = false; - - scene = new THREE.Scene(); - camera = new THREE.PerspectiveCamera(); - camera.matrixAutoUpdate = false; - - const light = new THREE.DirectionalLight(0xffffff, 1); - light.position.set(10, 10, 10); - scene.add(light); - - const loader = new THREE.GLTFLoader(); - loader.load("https://immersive-web.github.io/webxr-samples/media/gltf/reticle/reticle.gltf", (gltf) => { - reticle = gltf.scene; - reticle.visible = false; - scene.add(reticle); - }); - - currentSession = await navigator.xr.requestSession('immersive-ar', { - optionalFeatures: ["dom-overlay"], - domOverlay: { root: document.body }, - requiredFeatures: ['hit-test'] - }); - - currentSession.updateRenderState({ baseLayer: new XRWebGLLayer(currentSession, gl) }); - const referenceSpace = await currentSession.requestReferenceSpace('local'); - const viewerSpace = await currentSession.requestReferenceSpace('viewer'); - const hitTestSource = await currentSession.requestHitTestSource({ space: viewerSpace }); - - document.getElementById('menu-bar').style.display = 'flex'; - - currentSession.addEventListener("end", () => { - currentSession = null; - menus.forEach(id => { - document.getElementById(id).style.display = 'none'; - }); - }); - - canvas.addEventListener("pointerdown", selectModelFromScene); - - currentSession.requestAnimationFrame(function onXRFrame(time, frame) { - currentSession.requestAnimationFrame(onXRFrame); - gl.bindFramebuffer(gl.FRAMEBUFFER, currentSession.renderState.baseLayer.framebuffer); - - const pose = frame.getViewerPose(referenceSpace); - if (pose) { - const view = pose.views[0]; - const viewport = currentSession.renderState.baseLayer.getViewport(view); - renderer.setSize(viewport.width, viewport.height); - - camera.matrix.fromArray(view.transform.matrix); - camera.projectionMatrix.fromArray(view.projectionMatrix); - camera.updateMatrixWorld(true); - - const hitTestResults = frame.getHitTestResults(hitTestSource); - if (hitTestResults.length > 0) { - const hitPose = hitTestResults[0].getPose(referenceSpace); - reticle.visible = true; - reticle.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z); - reticle.updateMatrixWorld(true); - } - - renderer.render(scene, camera); - } - }); - } - - function exitAR() { - document.getElementById('confirmation-dialog').style.display = 'flex'; - } - - function confirmExit(shouldExit) { - if (shouldExit && currentSession) { - currentSession.end(); - } - document.getElementById('confirmation-dialog').style.display = 'none'; - } - - let soundTimeout = false; - - function playButtonSound() { - if (!soundTimeout) { - const sound = document.getElementById("button-sound"); - sound.currentTime = 0; - sound.play(); - - soundTimeout = true; - setTimeout(() => { - soundTimeout = false; - }, 200); // Verzögerung von 200ms - } - } - - - if (navigator.xr) { - const startButton = document.createElement('button'); - startButton.textContent = 'Start AR'; - startButton.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; - document.body.appendChild(startButton); - startButton.onclick = () => { - startButton.remove(); - activateXR(); - }; - } else { - alert('WebXR wird nicht unterstützt.'); - } - - </script> </body> </html> diff --git a/public/previewImages/download-icon.png b/public/previewImages/download-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..663c90144dd713cc4abddfb476d1dfa428571231 GIT binary patch literal 7008 zcmeHMXH?VKwm%6`f^?AqMG1n6N{Mu&B_kk$0ud2V1Z*IP^j;HnsE#s96{HO!AOaSe zQUU^^FqEN)NC`#6&_WA@5R!M|z3Z*_@qT#kuJ_^o*UHL&XP5IkWuILVN3bvv+_7f| z06@_6l#vwxNcf5b=<V=pHMoxnzYu;_CMQ5?=Uyr#P&a-x{}q5&3H+=p+aS&7ed>ZA z07A{&4<cDXND6>GbW@{Wt%DusN5XD7PDgz4>3v5CI$Lhm-Z%14tk3n4_{qS=-=6VQ z?($GU%^n*RN<ZW=;NtqXz|yez-54PFxy1Oh?7pu8M?P;;cwN1K3$XQ{CFR;*6tqh< zO3XV_Ic0~%zWZaB=~)S@*G3n^gf_e%dYGmZuGi9zPp@x)=8#SW`LU@d>+AZ{5l8eP z#((J%>`$!1H;q*LTs!6=q&z?Jc|0pCOD#lDJq?hy+e<TB6EMW9v!6>W7rs`w(|WRp zzviD#Ev;W}Q{eawReEr?8gBEH<;S29pwC!^5fdGqrSJFsufcN1SL5U3V$)sOoKNM7 zCy=1<&#Q!j+^xsz<ya+)Ma|<8%+;dyR4h?eDGC5<W0kbt)?Az7fLh5f&c{QRzY$HP zL;)DF5%~7_@#8LKiyuOgQc@Xd+1W%Lu7baz%5S}RHf!Sg!pmaYGJ_0FSpam3B<m+9 zC&~5N_iKz$0M9FKSe00{MY|pu$u45KA%L@)-+FfsW4`|HzyHoH_mKdg&01gx;!Kq& z@G=$<6pYaDFajWHYL(hceyJW<+;SQL2#?fn@M14jAFEOKEp~Qw{c-ARH9yc;x;%>1 zIHsz)+?S$cula4nY_KMXwvJU)WZ_4*9kl>>^If(Bb#-+dMVd{|-+3m)1K8W~D`kii z=1+lkk|tJh{(SG;+?<q@mS=b$Sq9xdEd=_SUtMcuhdSEW>|xjO^YddmYw%Hgpz!v@ z8mH1@C=#0bT1DQP7oIvuxPk;T<71@`HEZ7k@*Up#8Uwr_8swA+?bTjHkcH6w@_<x1 zL!(h1Y4WD7k)H1aK;#bqNF)^KYk2?uj-R-zg5}Zw{tciG03aiSfXAK>0Q4yU@ObDF z$p4}CACdp8%m2IcQkY#qoh<vV^dc63!ff&*1GdTN`*<`UF^{Jkux<YN{x-#ct?G5f z7zHo#f6F-!UH+x;<;KQFflbLpdiZF`UHP!kP_|52Q7rU#foC0!`E|U~+5t0tW5dIj zDA9saE<LTch(t~l%Os|^i+0Abmw=Rs&9G#>diClyl}c@#9VqiUvNllap!+6Z_VZF> zW8>0QoX@iD@8EKNO4ru4)&~(ka$}@Jbuy=QQ`z6vT;OB*M0=^i1QW4}^Ptpkg@=Ch zL-v{aOcW#-Qkb8sBIN|P{M;gJJG1n4ZCGAC%LL}(EWIDEWx+2*ZMx54;*7s3bGN`V zzm4hM^X$c*gzHllS$Z4tZj&3}d8xK(SElaz8)`E~$zpU<MMo!8b&D8rR4J3;Z~#=> zQ^Rn<Y}Q7$L89W;;jab#H|x?gv-opv#*3}IkK)f(oNUIPxa{e<(AwMciLwnzs(DyX zySs6-PF(`)SgWd`L05C5z6H;7w0CnOij$t<X>~n4R&u$|L{J(Sv*=k5d#Qc$!-o%t zQtG#+PIql1r*h3_#3y=Rzy6Kh^5!*9PaMbhtM-!aDd4S34bu;%&-C9&t)I`NGiS>< z)8$K-z{oLO-5S5}@bH^d|G6jO`Qyzr5ky1DQ`*W{h5LZPn^h&6kSM{4Et|7P!Q!3& zGFj)?vF>clOj-;kk`eZ-pkRcLq<qoNj<J}o;T|bR3v9_tFkm*x$;pvt-sVK2@aE$3 zLqC4}a9X9)J^m67xCHk%Wo5dHJkuQ0sj$Fys1M%=TJf}<AAB=CnV6VJ)S_95A#<YR zZ|kb7uTG8B1mV&#GwB{M%BE*!MH-{<(`U@h=Pjm(XYH#z9Uzi-qobqPVCg~$1i?^E zO$}G>MX68tHc~nZul+d-;D6F*ihQ&GxviGx@RtnC^WQo$w8~CP9CBLKBVdp@F7b)8 zFs5^KXg1Nh2txf)#|?_2Lt41H2b)9Iz*q2*(z%h?(tpyN7?f8nCWFk`+p<WTpN}ZH zY0LH+xLIK^emqASg}=W`zW<qdMx+YO<`J5-R@}abouiOBCOsb<NP9$phVRluOM*3h zhT}3S2IkN853p$0uU?(Vt$q3)D%Ub+id$ygE5`H@-kj&`bk4khurSBf(dr5^=TQ8w z%=-1ZGF2LjEVY1YXzO2UBMx3m&|kaTxj7>S=sw=<DN0hy?=*+`NMW`E3LNJR?BNvy zQoI<@I6FPiSF0kj;FvkOXi^7fBZ<Kf6|wH7sD>K%p2h4ZPv+WBA2owfH(9}U?VGbl zh$R?y%ye2IC$G|Pefr62{ajn3LE|F@J>PU^SNU!+1F%=DpMb(Rm?Uk~zR<_=HUw$V zH8{{GJH8(jKL~EhZfl^|-a=t{@hhI2l(~l{(!2jLiqQ_zr`lTrSYD9}VqTjR#i)*q z5L06aE0{4J<J8uE(np>Yn;t%88G$CHe}N}(u=dv8AY|;*>SZT8JJ-2n;y6r83VOaT z_DG*U2ntbN0tfC~Wt3fb^SuIYtQND416|7rUKYm_gNhC#2$&f7Buod$#esCiwneNS zn=#3e#}*w!5X6kl3*-~9QzIXCBGk`59VgE86^)nsY?8Ob4G|C$dNA#r2el6LEMi~4 z3}axJUB-(bU|ZTj*K|PM#ym*ByfMN)8_gNKe);lP!m_fsJh=I^aHcM7y*@2Ss>_R) za6@!-o)ZYKEn;Us5l)GDTu)PTEvnX2F9=zf=W5H2vMHt)L_|bzG-y^l+|&4O6lOou zCMQoiwhL=y=_2|}n-w@qOG|#6>oks=Er%`(ZWd~P3S1c87_61LqqW_N_wUigs#}|D z^VyoLSrgFL_xyY@n{{7nl+gkOQh5qr$5w6ex2zDB!jpoEL=l8@#vbcyYmosiF5gpE z4EF~gCmn68+9HclD~aPw`fS&(F2hpxtSRWT^XZ|}>01`@q^_x$6W;Qqqen_vwq=f6 zX784rQZYMnCE4zuUxYL+mSGVDzvIaIT}sG!U4$&jQz4qeVzJ_s>??156pp&g(7Fp; zfv61fhh7t9u}l}yEh39Ybsvu=k5f>{oL`Md=$vpau?v}Vg-hshi37--+gxG`Da;{W zWoRV=*Tc|;YxY<ZXI+rvvJ(g%LK=bfu0<_Yh8a+Qf)+u%crxzA(0UBecvLJ{$m9~4 z0DXc>WI@721|UTtz@QrpDfU@3=`yrGwkAMq!tF$e`~mfA6S-1y;Lc7cMN?%7a$-^V z%Mk2ksNlm#8iNY4f58Fe#6T<%6?w!Jmj`wSkVg0e5Oe|<I|QYTxZ;YUpf(EHG<ShS zcq$m%2bEoA;R;Nbpg<K@B~g+cjibR>p#&$Eo+HV26WPOF)I^eW;rKb7DKVVSCH8{E zU@1gH4fmG^5Nfur7#J3Z@-xj4M@SHi8*mx$`?#ipOn|o~9Hmf|JIVqdSa=KpoWBkM z3_~075HjH)gxOUm2419bSzMqmeg|XqkkxI@Wwk-A49FVcvW77Tygip?%4Kbdf%5y1 zmC0q5$G}7zj&+#3WW5GRQl%I~C;;Uw!}x&NeuznTH^ekA4-Rvc=DA8qhJbJsVqb`X zO6Q~zcq+7D#|P0CTtbo_h#+Lg9Jv#j5(ECfL#3c5I7g}g0&fxrM7`y?#;4Ih#{v~g zvR7dV5_L|4ZgIHa!`wZg>LKt0+$kP|oOgzx`@F17Ac|{lL=ODW5fxEGa@TKRC$PSQ zjul66t*!HcqlT#FSips_2L+O4k?H$>astrcs0m8_A38Thf%PM_tIbbq{c@mCQMCJa zG<Tfmrhwokn=>j(5a75El-Av37}BApC8|#inG!=}4#KDhJ>Z-Kl5{av8sYXfg!(Yl z&d%-&6s(|sQ|de6qoJp-zvZ{JwZ(O_YTv0gDR^VhI%<+rp&P;AWIH_~(z8fBB*>8v zAkLfs1l_Z8q#0FKksJcABrbYPO>L#9zP`TedrXeP@LEAB3RE0K{?Kg7))51J>F6AL zsAT~NVX`C(HI^F^ftQN~IxUL@M^NDYe&pedamz$B*kg<${L+;DOcba;K<A7I5YtbA z?o+a)KoyqTQ3U>499T$&f_z|)IqIcVG|pTexax^E)Ty%K@Id4addzb-+h{*P8=@9H z1&Feu;5S)h^}eQTR}>hJ1*4sd^0Ej#wC5a+JIe>&??gzljSc~{IjY&2A(jbb^h8Ax z#)XmKmpGEP0MSks$cQ02ZZMjH_&{|W+Euv;GinAJhGI$IRaoPhC@_2n?JCrS@iYew z9<d~*8f$zF{&*W*h;70woB<6rA_)35MpI4kb+F5_+Re|~&Fj;bFJIQ@zPy^eH0lue zh0j8MSL?Uc)zvQg`Z_KZ^XH%cTw^`0hRpMaKuQd|9ZP22zAvR^@b*Sm7SCy-Z`h4g z)+b&ZqY-CcakGEm$zqZz$~s$tsN)vm+0ofq>Q!-(5(bkj2kNW)REB-BG&$`-%co4; zpvW+IL95;4@JN(+oe$^vnKtI-<;nMkw)AjX|57NcT^=mgOEQSZ>?n8exn*u{zIAUR zE$<??v_11xd;Q3HpWiz=I<^=CWHw!*GaE+?SsJT3ZD?p{(!0{y_0)N8vYfH~QnmMZ zB6UaPttH*wlQ%k_Y;=?_8R3t_UptMmzBg_;7(eMA9L!LI?SXUmct;|t!#9@uCtDLZ z?d87*HC_yFl()=cd9`T$saRl<xhL~yn?e6bcVLUpLFDts#ey>}uaZkwiK>F^Pc1y9 z9We7W$kyz@Mc%_MbHk0@1<FVJ$7yMdit?pz_#yGuWb_Z^rfgO5R-5AfP@kpC3V(NZ zcUSd1d)w->)Z$PVN*`<~t86-HV#4wG8vH0{K7P^+<sUO{`8fXCY$PQk5UcE18yo-= zWci-g7h3(7wgtE|u)45k;#F{8p8sp9cBv8m#%^|!TVP<|jinFE`FSO8tk8O?njOb0 zr>Cd0FEV@Hx%>Jq+xC7)-0oq!*H$)1O@P>Dz1Ln$Ol(?0xKjC(#9wsWhkMwo{{H^1 zOU2JgvL}}dwtFNCVMPh5KcRpA`0-=VFT#}zx=FZ_P%|g|lDnAG1?I))`&(n>Ma<6b zwU^D=8;x^5`k*9H-jY2&(P)Ek3(Pu|!IY*^6S0x4uY7xiH&T}i&>pEmuO<efBSWaJ zdHW!Yz>Vc8OT~R2{bz@?UP@&97xyzM_neGdb`kjuzXq%3s<$jnS>VGY*-0hZ^_@?S z`voqMinDqgcLX2vYWMc0)JI_2X3Yj+&x#f&CLei}c4A1JNC|@ZwZsh<7lTyH)orX1 zw*UL)=4KO2$hU|4LaMByqoWN{WOI^$vJ!Z(_xN(q^6N)TCez^DXD#s^E!}3VgQ+m3 zCmzH#>mOrLf4eHAsHkYYmd&(~=)st0RIz5O=dH}ZE=o5KuTVI*r_<1YCo*$6_|Yh< zYO$B9(YQ>jzivNw+`_t7T-;oFUDEcB$NQhz>FWpkbl0?&cbZLFG}NiEs83TJL#bZV z&d&6;HFo&!pckk4NE4LRu6DDb`iM=FIsv?VwU~F108ytXm`#DHAf>$~Swma9TG!(% zTNQr<;Z5zyC#R$)r$jD>^*&1)uo;^l=Brj~%2w94tKIu#AUSZVEx96OP}4)0^k?DP z{LuPVh(vXqE~3hX(JKf$OU`uyqk&~)HgkQ-c_?YAMZ%a)aOClV`0RJH{oCQrXVt!_ zef#z;t)soN$b1W*2<O<mSdgLYRDX#Y#Gxo=$_*{Q_IS_R0O8q8AN03g)kVN)laj^R zTz<XW6Y}~XcIZ$z=%Z@Lq!&m=iqc^Gur~5g2ADHYPXPDh^^`8+tZc(#%uNwM_{ZA} zd=oWK08N<Zu$>j9t^tEqp)>-B_m;zelbenL)`W9hm6@G@a5mtlpg9!m|0!qy1#PRi zg3_?Zg?HczGNDDW^IX9hXz{-FPeBX<Xq>Cy3T8pUZeNuDX7)}^R&G6{tX2H)5zQDq z`KLL0xIPsL<x6Syu*6Fn7p5NsIUnN3YeH7OW4k&#BYj~K=^I&X4eLG1;Jbj?Yp@R) zISPk_*!)^VM$6rLi9cp%!<^vhzQ^)D(zD1(2wR)mc-bgSmj^lf<8y0?9CiV$(<5i* z2H#xU7_XF+mS&UT?pPL#b1VgkuG<kn&`Mw$R)7&3u#gCmjcUN1v7IZM!d-j_vuASr z0CRm|WYZ9iycUX6kA)rEw*8AW)c7sl-i5>?sH}|-TJ=k<u(p&)Vc)aQNJ~p&e%B&z zgfnRd$${C!WFm*Mei5gB&T_P}177w*`5pYH!}^2Z;YslN@^V}uL}HIpX7*if2%)72 zNv#<@{FAYs6+97_Fg;?X>-+8FtJSxWGPKGlT_AEwWw^H$Gd$k37$fs<>$Cjt3>3Ot z>EStC?Q__}(e7(;JzUy}mU|MG(+=Ry7ua_-S(_VNu^TNZmos^=H*6wK-r$d*A=z-) zBJEEW1&+v{P1XO4#!_Lm7A4%z06TONj@6P6svo6#oI45jX2G6#hdUTEps%hz>1wZZ z3j<Frm;Z7vs|(blA;G%a$h!b}=w)Uhd}EEgm~ag&g0xv4w!Tp5WS6=V&_cSx>Qhod zL&VJNY)ulhX$F#(;*fYF7#^p&1*YR>(oXPc{Nclgw#$2fU2aiP5z)4mtW2vkKY}2H z{|WyO*whM<fSgtgk>%=GdHzBWkd~B`WaNGN^obvGs!d94imU0AwINPa4>G5k|CpB6 z`rBH5Fr23!w#EwFUB%(g--h2GF+u)Wd~!MY{&da+w}jK9RXSLXrntbqJ#}?kxCWdP z?J9giVX?X8?64(aR>wm))x>eXcI}$e#~PUNe5nmt!72MOB+P_K6Z~Jeg(NI~v%7>5 zph^X%m<lkh<h-f1Pw(N$As97`)`ewzR9qycV~}9<JM=zsKZU|+d0ocl&nNSaxxj{r z<X-5E^j$obQ!#*a9fndTZWFCaM}@_|Pmpv%6mjm{x$H>TbBjEJLxC$W(eZTYeP&K3 z46o7p7P0<%V23g4X-6YGOw_H-bxudV2|^|%H#ZkITIEH-&3qek`kV~xY<_Eid00Yc zM~8kDH;3N10$j`08602e*yIbWXP301iMhpymoLz_qoSgOG5VpaF_g{Gb@p9uGpC&h z^H>7Qlfz&zpfhh$IP~(cYWusy3kQ*K7Ik_ry+T8on^ux@A&ZT?RnHRW)TIZk&q(mX zI{n%JC48l$*iE<2r6YYH2v*fFq%~MG^Gbyy{-uA80C276m1Yq0P`4b<=9jN=l}wE- Kj7m>jzVl!BdeS!l literal 0 HcmV?d00001 diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..b34fa5f --- /dev/null +++ b/public/styles.css @@ -0,0 +1,207 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #f0f0f0; + color: #333; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url('https://www.hft-stuttgart.de/fileadmin/Dateien/Hochschule/-_R_Juergen_Pollak_HFT_18.04.18-0091.jpg'); + background-size: cover; + background-position: center; + } + + .container { + text-align: center; + background-color: rgba(255, 255, 255, 0.9); + padding: 50px; + border-radius: 10px; + color: #333; + max-width: 80%; + box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2); + } + + h1 { + font-size: 3em; + margin-bottom: 20px; + color: #444; + } + + p { + font-size: 1.2em; + margin-bottom: 30px; + } + + #add-menu { + overflow-y: auto; + + } + + #menu-bar, + .menu-placeholder { + position: absolute; + bottom: 0; + width: 100%; + height: 80px; + display: flex; + justify-content: space-around; + align-items: center; + background: #121212; + padding: 10px; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.8); + color: white; + z-index: 10; + border-top: 1px solid #2a2a2a; + } + + .menu-item { + background: #2c2c2c; + border-radius: 20px; + padding: 8px; + transition: background-color 0.3s, transform 0.2s; + } + + .menu-item:hover { + background-color: #3a3a3a; + transform: scale(1.1); + } + + .menu-item img { + width: 50px; + height: 50px; + } + + .menu-placeholder .menu-item img { + width: 50px; + height: 50px; + } + + button { + background-color: #4CAF50; + color: white; + font-size: 1em; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease-in-out, transform 0.2s; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 10px; + } + + button:hover { + background-color: #45a049; + } + + button:active { + background-color: #387a39; + } + + /* Confirmation Dialog */ + #confirmation-dialog, + #delete-confirmation-dialog, + #info-dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 20; + } + + .dialog-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + } + + .dialog-box { + position: relative; + background: #2c2c2c; + padding: 20px; + border-radius: 10px; + text-align: center; + color: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8); + } + + .dialog-box p { + margin-bottom: 20px; + font-size: 1.2em; + color: #f0f0f0; + } + + .dialog-box button { + margin: 5px; + padding: 10px 20px; + font-size: 16px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s; + } + + .dialog-box button:first-child { + background-color: #e74c3c; + color: white; + } + + .dialog-box button:first-child:hover { + background-color: #c0392b; + transform: scale(1.05); + } + + .dialog-box button:last-child { + background-color: #3498db; + color: white; + } + + .dialog-box button:last-child:hover { + background-color: #2980b9; + transform: scale(1.05); + } + + + #dynamic-menu { + position: absolute; + bottom: 80px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + background: #1e1e1e; + box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.8); + color: white; + width: 100vw; + height: 200px; + z-index: 20; + overflow-y: auto; + border-radius: 8px; + } + + + #dynamic-menu input[type="range"] { + width: 100%; + margin: 10px auto; + background: #3a3a3a; + border-radius: 5px; + } + + #dynamic-menu input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: #fff; + border: 2px solid #555; + border-radius: 50%; + cursor: pointer; + } \ No newline at end of file -- GitLab