RH MediaTour wird geladen …
`);return;} const a=document.createElement('a');a.href=url;a.download=`snapshot-${slug}-${scene?.id||'szene'}.${fmt==='jpeg'?'jpg':'png'}`;a.click(); }catch(e){alert(e.message||'Snapshot fehlgeschlagen');}} function trackSceneHit(){ if(!scene||sessionStorage.getItem('rh360-stat-'+slug+'-'+scene.id))return; sessionStorage.setItem('rh360-stat-'+slug+'-'+scene.id,'1'); fetch('/api/tours/'+slug+'/stats/hit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sceneId:scene.id})}).catch(()=>{}); } function showOfflineScreen(){document.body.innerHTML=`
RH Media

Die Tour ist aktuell offline oder noch als Entwurf gespeichert.

Bitte versuchen Sie es später erneut oder kontaktieren Sie RH Media.

`;} /* RH360 v12.1 map + popup + scene dock hotfix */ const RH360_DEFAULT_GEO_VIEW = {lat:48.9000,lng:15.2200,zoom:14}; function vProjectGeo(lat,lng,z){const n=Math.pow(2,z); const x=(lng+180)/360*n*256; const r=lat*Math.PI/180; const y=(1-Math.log(Math.tan(r)+1/Math.cos(r))/Math.PI)/2*n*256; return {x,y};} function vGeoCenter(){ const geos=(tour?.scenes||[]).map(s=>s.geo).filter(g=>g&&Number.isFinite(g.lat)&&Number.isFinite(g.lng)); if(scene?.geo)return scene.geo; if(geos.length)return {lat:geos.reduce((a,g)=>a+g.lat,0)/geos.length,lng:geos.reduce((a,g)=>a+g.lng,0)/geos.length,zoom:geos[0].zoom||14}; return RH360_DEFAULT_GEO_VIEW;} function renderViewerOsmTiles(map,center,zoom){map.querySelectorAll('.osm-tile,.overview-empty').forEach(e=>e.remove()); map.classList.add('osm'); const w=map.clientWidth||320,h=map.clientHeight||230; const c=vProjectGeo(center.lat,center.lng,zoom); const sx=Math.floor((c.x-w/2)/256)-1,ex=Math.floor((c.x+w/2)/256)+1,sy=Math.floor((c.y-h/2)/256)-1,ey=Math.floor((c.y+h/2)/256)+1; for(let x=sx;x<=ex;x++){for(let y=sy;y<=ey;y++){const img=document.createElement('img');img.className='osm-tile';img.loading='lazy';img.referrerPolicy='no-referrer';img.src=`https://tile.openstreetmap.org/${zoom}/${x}/${y}.png`;img.style.left=(x*256-(c.x-w/2))+'px';img.style.top=(y*256-(c.y-h/2))+'px';map.appendChild(img);}}} function vGeoToMap(g,center,zoom,map){const w=map.clientWidth||320,h=map.clientHeight||230;const c=vProjectGeo(center.lat,center.lng,zoom),p=vProjectGeo(g.lat,g.lng,zoom);return{x:(p.x-(c.x-w/2))/w,y:(p.y-(c.y-h/2))/h};} renderOverviewMap=function(){ const map=document.getElementById('overviewMap'); if(!map||!tour)return; map.innerHTML=''; const geoPts=(tour.scenes||[]).filter(s=>s.geo&&Number.isFinite(s.geo.lat)&&Number.isFinite(s.geo.lng)); const oldPts=(tour.scenes||[]).filter(s=>s.map&&typeof s.map.x==='number'); const btn=document.getElementById('mapBtn'); if(!geoPts.length&&!oldPts.length){ if(btn)btn.classList.add('hidden-map'); map.innerHTML='
Keine Kartenpositionen gespeichert.
'; return;} if(btn)btn.classList.remove('hidden-map'); if(geoPts.length){const center=vGeoCenter(); const zoom=Math.max(12,Math.min(17,center.zoom||14)); renderViewerOsmTiles(map,center,zoom); (tour.scenes||[]).forEach((s,i)=>{if(!s.geo)return;const pos=vGeoToMap(s.geo,center,zoom,map);const b=document.createElement('button');b.className='ov-point'+(s.id===scene?.id?' active':'');b.style.left=(Math.max(.03,Math.min(.97,pos.x))*100)+'%';b.style.top=(Math.max(.03,Math.min(.97,pos.y))*100)+'%';b.textContent=i+1;b.title=s.title;b.onclick=()=>goScene(s.id);map.appendChild(b);});return;} map.classList.remove('osm'); oldPts.forEach((s,i)=>{const b=document.createElement('button');b.className='ov-point'+(s.id===scene?.id?' active':'');b.style.left=(s.map.x*100)+'%';b.style.top=(s.map.y*100)+'%';b.textContent=i+1;b.title=s.title;b.onclick=()=>goScene(s.id);map.appendChild(b);}); }; const _centerActiveScene_v121 = centerActiveScene; centerActiveScene=function(){const dock=document.getElementById('dock'); const active=dock?.querySelector('.scene-pill.active'); if(active) requestAnimationFrame(()=>active.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'}));}; const _showPopup_v121=showPopup; showPopup=function(h){ _showPopup_v121(h); document.querySelectorAll('#popLinks svg,#popMeta svg').forEach(svg=>{svg.setAttribute('width','18');svg.setAttribute('height','18');svg.style.width='18px';svg.style.height='18px';}); }; /* RH360 v12.2 viewer fixes */ function rh360IsEmojiLike(v){return !!String(v||'').trim() && !/^[a-z0-9_-]+$/i.test(String(v||'').trim()) && !/^https?:/i.test(String(v||''));} function rh360MarkerInner(h){ if(h.iconUrl) return ``; const raw=(h.icon||'').trim(); if(raw && rh360IsEmojiLike(raw)) return `${esc(raw)}`; return svgIcon(raw || h.type || 'info'); } mkMarkerHtml=function(h){ if(h.type==='scene'){ const tgt=(tour.scenes||[]).find(s=>s.id===h.targetSceneId); const img=h.iconUrl||tgt?.thumb||tgt?.panorama||''; return `
`; } const custom = h.icon && rh360IsEmojiLike(h.icon) ? ' custom-text' : ''; return `
${rh360MarkerInner(h)}
`; }; function rh360TypeOk(h,type){ if(type==='all')return true; if(type==='poi')return h.type!=='scene'; return (h.type||'info')===type; } buildSearch=function(){ const q=(document.getElementById('searchInput')?.value||'').toLowerCase().trim(); const scope=document.getElementById('searchScope')?.value||'current'; const type=document.getElementById('searchType')?.value||'poi'; const scenes = scope==='current' && scene ? [scene] : (tour.scenes||[]); const rows=[]; scenes.forEach(sc=>(sc.hotspots||[]).forEach(h=>{ if(!rh360TypeOk(h,type)) return; const hay=[h.title,h.text,h.link,h.phone,h.email,h.address,h.hours,sc.title].filter(Boolean).join(' ').toLowerCase(); if(!q || hay.includes(q)) rows.push({sc,h}); })); const box=document.getElementById('searchResults'); box.innerHTML=rows.length?rows.map(r=>``).join(''):'

Keine Treffer.

'; document.querySelectorAll('#searchResults button').forEach(b=>b.onclick=()=>{goScene(b.dataset.scene);document.getElementById('searchPanel').classList.add('hidden');setTimeout(()=>{const h=(scene.hotspots||[]).find(x=>x.id===b.dataset.hs);if(h){focusHotspot(h);setTimeout(()=>handleHotspot(h),260);}},420);}); }; document.getElementById('searchScope')?.addEventListener('change', buildSearch); document.getElementById('searchType')?.addEventListener('change', buildSearch); const _rh360SetupStart=setupStartScreen; setupStartScreen=function(){ if(tour?.dsgvo && !localStorage['rh360-consent-'+slug]) return; _rh360SetupStart(); }; const _rh360RenderOverlay=renderOverlay; renderOverlay=function(){ _rh360RenderOverlay(); setTimeout(()=>{try{centerActiveScene();renderOverviewMap();}catch{}},50); }; async function rh360LoadImageSafe(src){return new Promise(res=>{const img=new Image();img.crossOrigin='anonymous';img.onload=()=>res(img);img.onerror=()=>res(null);img.src=src;});} async function rh360BuildSnapshotCanvas(){ const srcCanvas=document.querySelector('#pano canvas'); const w=srcCanvas?.width||1920,h=srcCanvas?.height||1080; const out=document.createElement('canvas'); out.width=w; out.height=h; const ctx=out.getContext('2d'); let used=false; try{ if(srcCanvas){ctx.drawImage(srcCanvas,0,0); const d=ctx.getImageData(Math.floor(w/2),Math.floor(h/2),1,1).data; used=(d[0]+d[1]+d[2])>8; }}catch{} if(!used){ const img=await rh360LoadImageSafe(scene?.panorama||''); if(img){ const ratio=Math.max(w/img.width,h/img.height); const iw=img.width*ratio, ih=img.height*ratio; ctx.drawImage(img,(w-iw)/2,(h-ih)/2,iw,ih); } else { ctx.fillStyle='#10131a';ctx.fillRect(0,0,w,h); } } ctx.fillStyle='rgba(0,0,0,.52)';ctx.fillRect(22,22,Math.min(620,w-44),92); ctx.fillStyle='#fff';ctx.font='bold 30px system-ui';ctx.fillText(tour.title||'Tour',42,60);ctx.font='22px system-ui';ctx.fillText(scene?.title||'',42,94); if(tour.client?.logo){const cl=await rh360LoadImageSafe(tour.client.logo); if(cl)try{ctx.drawImage(cl,w-184,24,152,62)}catch{}} const logo=await rh360LoadImageSafe('/assets/logo/logo_weiss.png'); ctx.fillStyle='rgba(0,0,0,.52)';ctx.fillRect(w-258,h-74,236,52); if(logo)try{ctx.drawImage(logo,w-248,h-62,92,28)}catch{} ctx.fillStyle='#fff';ctx.font='bold 18px system-ui';ctx.fillText('RH Media 360°',w-145,h-39); return out; } downloadSnapshot=async function(fmt){try{const out=await rh360BuildSnapshotCanvas(); const type=fmt==='jpeg'?'image/jpeg':'image/png'; const url=out.toDataURL(type,0.94); if(fmt==='pdf'){const w=open('','_blank');w.document.write(`Snapshot
`);return;} const a=document.createElement('a');a.href=url;a.download=`snapshot-${slug}-${scene?.id||'szene'}.${fmt==='jpeg'?'jpg':'png'}`;a.click();}catch(e){alert(e.message||'Snapshot fehlgeschlagen');}}; /* RH360 v12.7 – eigener WebGL-360-Viewer als stabile Viewer-Engine */ (function(){ let rh360gl = null; function loadScriptOnce(src){ return new Promise((resolve,reject)=>{ if(document.querySelector(`script[src="${src}"]`)) return resolve(); const el=document.createElement('script'); el.src=src; el.onload=resolve; el.onerror=()=>reject(new Error('Konnte nicht laden: '+src)); document.head.appendChild(el); }); } function clamp(v,min,max){return Math.max(min,Math.min(max,v));} function makeViewerApi(state){ return { isRH360GL:true, destroy(){ try{state.disposed=true; window.removeEventListener('resize',state.onResize); state.renderer?.dispose?.(); state.texture?.dispose?.(); state.geometry?.dispose?.(); state.material?.dispose?.();}catch{} }, getPosition(){ return {longitude:state.lon, latitude:state.lat}; }, rotate(pos){ if(typeof pos?.longitude==='number') state.lon=pos.longitude; if(typeof pos?.latitude==='number') state.lat=clamp(pos.latitude,-1.35,1.35); state.update(); }, zoom(z){ state.zoom=clamp(Number(z)||68,20,90); state.camera.fov=clamp(94-state.zoom,62,92); state.camera.updateProjectionMatrix(); state.update(); }, resize(){ state.onResize(); }, needsUpdate(){ state.update(); }, startAutorotate(){}, stopAutorotate(){}, animate(pos){ this.rotate(pos); if(pos?.zoom) this.zoom(pos.zoom); } }; } async function createRH360Viewer(container, panorama, opts={}){ await loadScriptOnce('/lib/psv/three.min.js'); if(!window.THREE) throw new Error('Three.js nicht verfügbar'); const THREE=window.THREE; container.innerHTML=''; container.classList.add('rh360-webgl-active'); const markerLayer=document.createElement('div'); markerLayer.className='rh360-marker-layer'; const renderer=new THREE.WebGLRenderer({antialias:true,alpha:false,preserveDrawingBuffer:true}); renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2)); renderer.setSize(container.clientWidth||window.innerWidth,container.clientHeight||window.innerHeight); renderer.domElement.className='rh360-webgl-canvas'; container.appendChild(renderer.domElement); container.appendChild(markerLayer); const scn=new THREE.Scene(); const camera=new THREE.PerspectiveCamera(clamp(94-(opts.zoom||14),62,92),(container.clientWidth||1)/(container.clientHeight||1),0.1,1100); const geometry=new THREE.SphereGeometry(500,64,48); geometry.scale(-1,1,1); const texture=await new Promise((resolve,reject)=>{ const loader=new THREE.TextureLoader(); loader.setCrossOrigin('anonymous'); loader.load(panorama,t=>resolve(t),undefined,e=>reject(e)); }); texture.minFilter=THREE.LinearFilter; texture.magFilter=THREE.LinearFilter; const material=new THREE.MeshBasicMaterial({map:texture}); const mesh=new THREE.Mesh(geometry,material); scn.add(mesh); const state={disposed:false,renderer,texture,geometry,material,scene3:scn,camera,markerLayer,lon:Number(opts.longitude||0),lat:clamp(Number(opts.latitude||-0.02),-1.35,1.35),zoom:Number(opts.zoom||14),update:null,onResize:null}; function cameraTarget(){ const phi=Math.PI/2-state.lat; const theta=-state.lon; const x=500*Math.sin(phi)*Math.sin(theta), y=500*Math.cos(phi), z=500*Math.sin(phi)*Math.cos(theta); camera.lookAt(x,y,z); } function project(lon,lat){ const phi=Math.PI/2-lat, theta=-lon; const v=new THREE.Vector3(500*Math.sin(phi)*Math.sin(theta),500*Math.cos(phi),500*Math.sin(phi)*Math.cos(theta)); v.project(camera); if(v.z>1) return null; return {x:(v.x+1)/2*(container.clientWidth||1), y:(-v.y+1)/2*(container.clientHeight||1), visible:v.z<1}; } state.update=function(){ if(state.disposed) return; cameraTarget(); renderer.render(scn,camera); markerLayer.querySelectorAll('[data-rh360-marker]').forEach(el=>{ const lon=Number(el.dataset.lon), lat=Number(el.dataset.lat); const p=project(lon,lat); if(!p||!p.visible){el.style.display='none';return;} el.style.display='block'; el.style.transform=`translate3d(${p.x}px,${p.y}px,0) translate(-50%,-50%)`; }); }; state.onResize=function(){ const w=container.clientWidth||window.innerWidth, h=container.clientHeight||window.innerHeight; renderer.setSize(w,h); camera.aspect=w/h; camera.updateProjectionMatrix(); state.update(); }; let down=false,lastX=0,lastY=0; container.addEventListener('pointerdown',e=>{down=true;lastX=e.clientX;lastY=e.clientY;container.setPointerCapture?.(e.pointerId);}); container.addEventListener('pointermove',e=>{ if(!down)return; const dx=e.clientX-lastX, dy=e.clientY-lastY; lastX=e.clientX; lastY=e.clientY; state.lon-=dx*0.0032; state.lat=clamp(state.lat-dy*0.0032,-1.35,1.35); state.update(); }); container.addEventListener('pointerup',e=>{down=false;try{container.releasePointerCapture?.(e.pointerId)}catch{}}); container.addEventListener('pointercancel',()=>{down=false;}); container.addEventListener('wheel',e=>{e.preventDefault(); state.zoom=clamp(state.zoom+(e.deltaY>0?-4:4),10,42); camera.fov=clamp(94-state.zoom,62,92); camera.updateProjectionMatrix(); state.update();},{passive:false}); window.addEventListener('resize',state.onResize); const api=makeViewerApi(state); state.api=api; state.update(); return state; } window.rh360RenderMarkers = function(){ if(!rh360gl || !scene) return; const layer=rh360gl.markerLayer; layer.innerHTML=''; (scene.hotspots||[]).forEach(h=>{ const el=document.createElement('button'); el.type='button'; el.className='rh360-webgl-marker'; el.dataset.rh360Marker='1'; el.dataset.lon=hsLng(h); el.dataset.lat=hsLat(h); el.innerHTML=mkMarkerHtml(h); el.title=h.title||''; el.addEventListener('pointerdown',ev=>{ev.stopPropagation();},{passive:true}); el.addEventListener('pointerup',ev=>{ev.stopPropagation();},{passive:true}); el.onclick=ev=>{ev.preventDefault();ev.stopPropagation();handleHotspot(h);}; layer.appendChild(el); }); rh360gl.update(); }; const oldDrawMarkers = typeof drawMarkers==='function' ? drawMarkers : null; drawMarkers = function(){ if(rh360gl){ window.rh360RenderMarkers(); return; } if(oldDrawMarkers) oldDrawMarkers(); }; renderScene = async function(){ clearTimeout(readyTimer); if(viewer){try{viewer.destroy();}catch{}} viewer=null; markers=null; flatMode=false; if(rh360gl){try{rh360gl.api.destroy();}catch{} rh360gl=null;} const pano=document.getElementById('pano'); pano.innerHTML=''; if(!scene?.panorama){renderOverlay();return;} const sv=scene?.startView||{}; const startZoom=Math.max(2,Math.min(20,Number(sv.zoom ?? sv.defaultZoom ?? 8))); const startLong=Number(sv.longitude ?? sv.long ?? 0); const startLat=Number(sv.latitude ?? sv.lat ?? sv.pitch ?? -0.04); try{ rh360gl=await createRH360Viewer(pano,scene.panorama,{zoom:startZoom,longitude:startLong,latitude:startLat}); viewer=rh360gl.api; drawMarkers(); if(autorotate) doSpin(); } catch(e){ console.error('RH360 WebGL Viewer Fehler:',e); showFlat(); } renderOverlay(); setTimeout(()=>{try{viewer?.resize?.(); drawMarkers();}catch{}},250); }; })(); load(); /* RH360 v12.3 runtime overrides */ (function(){ // robust centered start screen, only after consent gate has disappeared if (typeof setupStartScreen === 'function') { setupStartScreen = function(){ try { if(!tour || tour._startDone || tour.startScreen===false) return; if(tour.dsgvo && !localStorage['rh360-consent-'+slug]) return; const ds=document.getElementById('dsgvoScreen'); if(ds && ds.style.display && ds.style.display !== 'none') return; tour._startDone=true; const sc=document.getElementById('startScreen'), card=document.getElementById('startCard'); const c=tour.client||{}; card.innerHTML=`${c.logo?``:`RH Media`}

${esc(tour.startHeadline||tour.title||'360° Tour')}

${esc(tour.startText||tour.description||'')}

${(c.name||c.phone||c.email||c.website)?``:''}`; sc.classList.add('on'); } catch(e) { console.warn('Startscreen', e); } }; } // legacy PSV zoom override disabled for RH360 WebGL viewer })(); /* RH360 v12.4 runtime polish */ (function(){ function forceViewerSize(){ try{ const p=document.getElementById('pano'); if(!p) return; p.style.position='absolute'; p.style.inset='0'; p.style.width='100%'; p.style.height='100%'; if(window.viewer){ viewer.needsUpdate?.(); viewer.resize?.({width:p.clientWidth,height:p.clientHeight}); } }catch(e){} } window.addEventListener('resize',()=>setTimeout(forceViewerSize,80)); const oldRenderScene = typeof renderScene==='function' ? renderScene : null; if(oldRenderScene){ renderScene = async function(){ await oldRenderScene.apply(this,arguments); setTimeout(forceViewerSize,250); setTimeout(forceViewerSize,900); }; } const oldSetupStart = typeof setupStartScreen==='function' ? setupStartScreen : null; if(oldSetupStart){ setupStartScreen=function(){ oldSetupStart.apply(this,arguments); setTimeout(()=>{ const card=document.getElementById('startCard'); if(card){card.style.textAlign='center';card.style.alignItems='center';} },50); }; } })();