// street.mjs — 3D terrain + tilt: toggle, heights, floor above the ground, URL round trip, sky toward the horizon const R_M = 12742000; const settle = `for (let i = 0; i < 80; i++) { await new Promise(r => setTimeout(r, 400)); const l = window.mapViewer.view.tiles; if (i > 2 && l.inflight === 0 && l.queue.length === 0) break; } const t = window.mapViewer.view.tiles, ready = [...t.tiles.values()].filter(e => e.state === 'ready');`; // declares t, ready export const steps = [ { name: '3D is on by default: tiles carry heights', wait: 4000, expect: v => v.terrain === 1 && v.withH > 0 && v.withH === v.ready && v.on, js: `window.mapViewer.view.setState({ lat: 46.5, lon: 9.8, alt: 0.0008 });` + settle + `return { terrain: t.terrain, ready: ready.length, withH: ready.filter(e => e.h).length, on: document.getElementById('nav-3d').classList.contains('on') };` }, { name: 'the camera stays 20 m above the loaded ground', expect: v => v.clear >= 19.9 && v.ground > 0, js: `const a = window.mapViewer; a.zoomBy(1e-6);` + settle + `const p = a.view.camera.position, e = p.length() - 1; const at = { lat: a.view.pose.lat, lon: a.view.pose.lon }, g = a.view.tiles.heightAt(at.lat, at.lon) ?? 0; return { clear: e * ${R_M} - g * a.state.terrain, ground: g };` }, { name: 'exaggeration slider reshapes and lands in the URL', expect: v => v.t === 3 && /3d=3/.test(v.h) && v.text === '3×', js: `const a = window.mapViewer, s = document.getElementById('exag'); s.value = '3'; s.dispatchEvent(new Event('input')); a.saveUrl(); return { t: a.view.tiles.terrain, h: location.hash, text: document.getElementById('exag-text').textContent };` }, { name: '3D off: flat tiles again, the slider hides, URL says 3d=0', expect: v => v.t === 0 && v.hidden && /3d=0/.test(v.h) && v.withH === 0, js: `const a = window.mapViewer; document.getElementById('nav-3d').click();` + settle + `a.saveUrl(); return { t: t.terrain, hidden: document.getElementById('exag-box').hidden, h: location.hash, withH: ready.filter(e => e.h).length };` }, { name: 'URL brings 3D back at its exaggeration', expect: v => v === 2, js: `location.hash = location.hash.replace(/3d=0/, '3d=2'); await new Promise(r => setTimeout(r, 300)); return window.mapViewer.view.tiles.terrain;` }, { name: 'tilted near the ground: sky at the top, ground at the bottom', js: `const a = window.mapViewer; a.view.setState({ lat: 46.5, lon: 9.8, alt: 0.0003, hd: 20, pt: 78 });` + settle + ` const gl = a.renderer.getContext(); a.view.render(); const W = gl.drawingBufferWidth, H = gl.drawingBufferHeight, px = new Uint8Array(4); gl.readPixels(W >> 1, H - 4, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); const top = [...px]; gl.readPixels(W >> 1, 4, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); const bottom = [...px]; return { top, bottom };`, expect: v => v.top[2] > 150 && v.top[2] > v.top[0] && Math.abs(v.bottom[2] - v.top[2]) + Math.abs(v.bottom[0] - v.top[0]) > 30 }, { name: 'screenshot for a human look', js: 'return 1', shot: process.env.SHOT || '/dev/null' }, { name: 'a click picks the 3D ground it shows; a marker there lands on the same pixel', expect: v => v.moved > 0.001 && v.dx < 1 && v.dy < 1, js: `const a = window.mapViewer, W = a.el.clientWidth, H = a.el.clientHeight, x = W / 2, y = H * 0.6; const p = a.view.pick(x, y), sea = a.view.pick(x, y, false), s = a.view.toScreen(p.lat, p.lon); return { moved: Math.hypot(p.lat - sea.lat, p.lon - sea.lon), dx: Math.abs(s.x - x), dy: Math.abs(s.y - y) };` }, { name: 'deep zoom: rivers are water in the tiles (overlay hidden); other lines lie on the 3D ground, depth-tested', expect: v => !v.riverLine && v.drape && v.depth && v.onGround, js: `const a = window.mapViewer; a.toggleOverlay('rivers', true); const R = await a.overlays.cache.get('rivers'), line = R.big.find(l => l.length > 6 && l.some(q => Math.abs(q.lat) < 50)); const i = Math.floor(line.length / 2), p = line[i], q = line[i + 1]; const hd = Math.atan2((q.lon - p.lon) * Math.cos(p.lat * Math.PI / 180), q.lat - p.lat) * 180 / Math.PI; a.view.setState({ lat: p.lat, lon: p.lon, alt: 0.0006, hd, pt: 72 });` + settle + ` a.showTab('measure'); a.measure.clear(); a.measure.add(p); a.measure.add({ lat: p.lat + 0.02, lon: p.lon + 0.02 }); a.measureChanged(); await new Promise(r => setTimeout(r, 800)); const o = a.view.lines.objects.get('measure'), arr = o.geometry.attributes.position.array; let best = null; // the draped vertex nearest the first measure point: compare with the drawn ground under it for (let k = 0; k < arr.length; k += 3) { const v = [arr[k] + o.position.x, arr[k + 1] + o.position.y, arr[k + 2] + o.position.z], r = Math.hypot(...v); const la = Math.asin(v[1] / r) * 180 / Math.PI, lo = Math.atan2(-v[2], v[0]) * 180 / Math.PI, d = Math.hypot(la - p.lat, lo - p.lon); if (!best || d < best.d) best = { d, h: (r - 1) * ${R_M}, g: (a.view.tiles.heightAt(la, lo, true) ?? 0) * a.state.terrain }; } a.measure.clear(); a.measureChanged(); return { riverLine: a.view.lines.objects.has('rivers'), drape: !!a.view.drape, depth: o.material.depthTest, onGround: best.d < 0.05 && Math.abs(best.h - best.g) < 60, ...best };` }, { name: 'screenshot: rivers on the ground', js: 'return 1', shot: process.env.SHOT2 || '/dev/null' }, { name: 'tilted 3D across the antimeridian: tiles with heights, finite picks', expect: v => v.withH >= 4 && v.errors === 0 && v.pick, js: `window.mapViewer.view.setState({ lat: 10, lon: 179.99, alt: 0.002, hd: 90, pt: 60 });` + settle + `const a = window.mapViewer; const p = a.view.pick(a.el.clientWidth / 2, a.el.clientHeight * 0.7); return { withH: ready.filter(e => e.h).length, errors: [...t.tiles.values()].filter(e => e.state === 'error').length, pick: !!p && Number.isFinite(p.lat) && Number.isFinite(p.lon) };` }, { name: 'tilted 3D near the north pole', expect: v => v.withH >= 1 && v.errors === 0 && v.pick, js: `window.mapViewer.view.setState({ lat: 89.9, lon: 0, alt: 0.002, hd: 0, pt: 60 });` + settle + `const a = window.mapViewer; const p = a.view.pick(a.el.clientWidth / 2, a.el.clientHeight * 0.7); return { withH: ready.filter(e => e.h).length, errors: [...t.tiles.values()].filter(e => e.state === 'error').length, pick: !!p && Number.isFinite(p.lat) && Number.isFinite(p.lon) };` }, ];