diff --git a/fes.js b/fes.js index e694fe5..9e617c5 100644 --- a/fes.js +++ b/fes.js @@ -1,21 +1,33 @@ -/* script.js — 完成版 - - CSV: data/floor1.csv / data/floor2.csv を想定 - - 期待ヘッダ: 名前 / 説明 / 画像ファイル / X座標 / Y座標 +/* script.js — 中央揃え・5フロア対応 完成版 + - CSV: data/floor1.csv ... data/floor4.csv / data/gym.csv + - CSVヘッダ(日本語 or 英語): 名前,name / 説明,description / 画像,image / X座標,x / Y座標,y */ 'use strict'; -const IMAGE_PATH = { 1: 'images/1F.png', 2: 'images/floor2.png' }; -const CSV_PATH = { 1: 'data/floor1.csv', 2: 'data/floor2.csv' }; +const IMAGE_PATH = { + 1: 'images/1F.png', + 2: 'images/floor2.png', + 3: 'images/floor3.png', + 4: 'images/floor4.png', + 5: 'images/gym.png' // 体育館 +}; +const CSV_PATH = { + 1: 'data/floor1.csv', + 2: 'data/floor2.csv', + 3: 'data/floor3.csv', + 4: 'data/floor4.csv', + 5: 'data/gym.csv' +}; // DOM const img = document.getElementById('mapImage'); -const map = document.getElementById('map'); // Panzoom 対象 -const wrapper = map.parentElement; // touch-action 切替のため +const map = document.getElementById('map'); +const wrapper = map.parentElement; const tabButtons = document.querySelectorAll('.tab-button'); const listToggle = document.getElementById('listToggle'); -const listPanel = document.getElementById('listPanel'); -const listContent= document.getElementById('listContent'); +const listPanel = document.getElementById('listPanel'); +const listContent = document.getElementById('listContent'); const closeListBtn = document.getElementById('closeList'); let currentFloor = 1; @@ -23,15 +35,11 @@ let pinEls = []; let tooltipEls = []; let panzoomInstance = null; - -// base image displayed size (measured when scale == 1) let baseImageWidth = 0; let baseImageHeight = 0; - -// track active pointer ids (for deciding multi-touch) const activePointers = new Set(); -/* ---------------- CSV パーサ(引用対応) ---------------- */ +/* ---------- CSV parser ---------- */ function parseCSV(text) { const rows = []; let cur = '', row = [], inQuotes = false; @@ -98,7 +106,7 @@ } } -/* 画像ロード待ち */ +/* ---------- Helpers ---------- */ function waitImageLoad(imgEl) { return new Promise(resolve => { if (imgEl.complete && imgEl.naturalWidth !== 0) return resolve(); @@ -106,12 +114,12 @@ imgEl.addEventListener('load', onLoad); }); } +function tick() { return new Promise(resolve => requestAnimationFrame(resolve)); } -/* ---------------- transform 行列から scale を読む(安全) ---------------- */ +/* get current scale from map transform */ function getCurrentScaleFromTransform() { const st = window.getComputedStyle(map).transform; if (!st || st === 'none') return 1; - // matrix(a,b,c,d,e,f) const m = st.match(/^matrix\((.+)\)$/); if (m) { const parts = m[1].split(',').map(Number); @@ -127,10 +135,8 @@ return 1; } -/* ---------------- touch-action の切替ロジック ---------------- - - scale > 1 または 複数指操作中 は map の操作を優先(touch-action: none) - - scale == 1 and single-finger -> ページ縦スクロール優先 (pan-y) -*/ +/* toggle wrapper touch-action: multi-touch or zoomed -> none (map handles gestures), + otherwise pan-y (page scroll allowed) */ function updateWrapperTouchAction() { const scale = getCurrentScaleFromTransform(); if (activePointers.size > 1 || scale > 1.001) { @@ -140,7 +146,7 @@ } } -/* ---------------- ツールチップの位置調整(viewport基準) ---------------- */ +/* adjust tooltip so it doesn't overflow viewport (calculations in viewport, apply in map-relative px) */ function adjustTooltipPosition(tooltip, desiredLeftPxMap, desiredTopPxMap) { const mapRect = map.getBoundingClientRect(); const desiredLeftViewport = mapRect.left + desiredLeftPxMap; @@ -153,21 +159,18 @@ const tRect = tooltip.getBoundingClientRect(); const margin = 8; - // horizontal adjustment in viewport coords let shiftXViewport = 0; if (tRect.left < margin) shiftXViewport = margin - tRect.left; if (tRect.right > window.innerWidth - margin) shiftXViewport = (window.innerWidth - margin) - tRect.right; - // vertical: if tooltip top would go above viewport, flip below pin let newTopViewport = desiredTopViewport; if (tRect.top < margin) { - tooltip.style.transform = 'translate(-50%, 8px)'; // below pin + tooltip.style.transform = 'translate(-50%, 8px)'; newTopViewport = desiredTopViewport + 44 + 10; } else { tooltip.style.transform = 'translate(-50%, -120%)'; } - // convert shiftXViewport -> map-relative px const shiftXMap = shiftXViewport; const newLeftMap = desiredLeftPxMap + shiftXMap; const newTopMap = newTopViewport - mapRect.top; @@ -177,21 +180,20 @@ tooltip.style.visibility = 'visible'; } -/* 閉じる */ +/* close all tooltips */ function closeAllTooltips() { tooltipEls.forEach(t => { if (t) { t.style.display = 'none'; t.setAttribute('aria-hidden','true'); } }); } -/* ---------------- ピン描画(基準サイズ baseImageWidth/baseImageHeight を使う) ---------------- */ +/* ---------- Pin & Tooltip rendering ---------- */ function placePins(data) { - // remove existing pinEls.forEach(p => p.remove()); tooltipEls.forEach(t => t.remove()); pinEls = []; tooltipEls = []; - // base sizes must be set (measured when scale == 1) + // base image size measured when scale == 1 const width = baseImageWidth || img.getBoundingClientRect().width; const height = baseImageHeight || img.getBoundingClientRect().height; @@ -199,7 +201,6 @@ const xPx = d.x * width; const yPx = d.y * height; - // pin const pin = document.createElement('div'); pin.className = 'pin'; pin.setAttribute('role','button'); @@ -214,7 +215,6 @@ pinImg.alt = ''; pin.appendChild(pinImg); - // stop propagation to avoid accidental pan start when tapping pin pin.addEventListener('pointerdown', e => e.stopPropagation()); pin.addEventListener('touchstart', e => e.stopPropagation(), {passive:true}); @@ -222,32 +222,26 @@ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); pin.click(); } }); - // tooltip card const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.dataset.index = String(idx); tooltip.setAttribute('aria-hidden','true'); const card = document.createElement('div'); card.className = 'tooltip-card'; - const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn'; closeBtn.type='button'; - closeBtn.textContent = '✕'; closeBtn.setAttribute('aria-label','閉じる'); - const header = document.createElement('div'); header.className='tooltip-header'; header.textContent = d.name || '(無題)'; - const body = document.createElement('div'); body.className='tooltip-body'; - const thumb = document.createElement('img'); thumb.className='tooltip-thumb'; - thumb.src = d.image ? d.image : 'images/placeholder.png'; - thumb.alt = d.name || ''; - const p = document.createElement('p'); p.className='desc'; p.textContent = d.description || ''; + const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn'; closeBtn.type='button'; closeBtn.textContent='✕'; closeBtn.setAttribute('aria-label','閉じる'); + const header = document.createElement('div'); header.className = 'tooltip-header'; header.textContent = d.name || '(無題)'; + const body = document.createElement('div'); body.className = 'tooltip-body'; + const thumb = document.createElement('img'); thumb.className = 'tooltip-thumb'; thumb.src = d.image ? d.image : 'images/placeholder.png'; thumb.alt = d.name || ''; + const p = document.createElement('p'); p.className = 'desc'; p.textContent = d.description || ''; body.appendChild(thumb); body.appendChild(p); card.appendChild(closeBtn); card.appendChild(header); card.appendChild(body); tooltip.appendChild(card); - // initial hidden position relative to base coords tooltip.style.left = xPx + 'px'; tooltip.style.top = yPx + 'px'; tooltip.style.display = 'none'; - // pin click toggles tooltip pin.addEventListener('click', (e) => { e.stopPropagation(); const isVisible = tooltip.style.display === 'block'; @@ -268,11 +262,9 @@ tooltip.setAttribute('aria-hidden','true'); }); - // prevent tooltip pointer events from causing map pan tooltip.addEventListener('pointerdown', ev => ev.stopPropagation()); tooltip.addEventListener('touchstart', ev => ev.stopPropagation(), {passive:true}); - // append to map (pin below tooltip so tooltip overlays) map.appendChild(pin); map.appendChild(tooltip); @@ -283,7 +275,7 @@ renderListPanel(data); } -/* ---------------- 一覧(ボトムシート)描画 ---------------- */ +/* ---------- List panel rendering ---------- */ function renderListPanel(data) { listContent.innerHTML = ''; data.forEach((d, idx) => { @@ -296,8 +288,8 @@ thumb.alt = d.name || ''; const meta = document.createElement('div'); meta.className = 'shop-meta'; - const title = document.createElement('p'); title.className='shop-title'; title.textContent = d.name || `スポット ${idx+1}`; - const desc = document.createElement('p'); desc.className='shop-desc'; desc.textContent = d.description || ''; + const title = document.createElement('p'); title.className = 'shop-title'; title.textContent = d.name || `スポット ${idx+1}`; + const desc = document.createElement('p'); desc.className = 'shop-desc'; desc.textContent = d.description || ''; meta.appendChild(title); meta.appendChild(desc); item.appendChild(thumb); item.appendChild(meta); @@ -310,23 +302,17 @@ listContent.appendChild(item); }); } - -/* open tooltip by index (and scroll page so map visible) */ function openTooltipByIndex(index) { if (!pinEls[index]) return; - // ensure map top visible + // scroll page so map top visible const topY = Math.max(0, map.getBoundingClientRect().top + window.scrollY - 80); window.scrollTo({ top: topY, behavior: 'smooth' }); pinEls[index].click(); } -/* ---------------- フロア読み込み ---------------- - ※ important: we reset panzoom BEFORE measuring base size, - so that baseImageWidth/height correspond to scale=1 displayed size. -*/ +/* ---------- load floor (reset panzoom, measure base image size, load CSV, place pins) ---------- */ async function loadFloor(floor) { if (currentFloor === floor) return; - tabButtons.forEach(btn => { const f = Number(btn.dataset.floor); if (f === floor) { btn.classList.add('active'); btn.setAttribute('aria-selected','true'); } @@ -337,13 +323,12 @@ img.src = IMAGE_PATH[floor]; await waitImageLoad(img); - // reset pan & measure base size (so pins placed relative to untransformed size) + // reset pan to identity so base image size is measured reliably if (panzoomInstance && typeof panzoomInstance.reset === 'function') { panzoomInstance.reset(); try { panzoomInstance.pan(0,0); } catch(e) {} } - // measure base displayed image size (after reset) - await tick(); // wait a frame + await tick(); const r = img.getBoundingClientRect(); baseImageWidth = r.width; baseImageHeight = r.height; @@ -352,32 +337,16 @@ currentData = data; placePins(data); - // update touch-action state based on current transform updateWrapperTouchAction(); } -/* ---------------- 一覧の open/close ---------------- */ -function openList() { - listPanel.classList.add('open'); - listPanel.setAttribute('aria-hidden','false'); - listToggle.setAttribute('aria-expanded','true'); - setTimeout(()=> listContent.focus(), 160); -} -function closeList() { - listPanel.classList.remove('open'); - listPanel.setAttribute('aria-hidden','true'); - listToggle.setAttribute('aria-expanded','false'); - listToggle.focus(); -} +/* ---------- list open/close ---------- */ +function openList() { listPanel.classList.add('open'); listPanel.setAttribute('aria-hidden','false'); listToggle.setAttribute('aria-expanded','true'); setTimeout(()=>listContent.focus(),160); } +function closeList() { listPanel.classList.remove('open'); listPanel.setAttribute('aria-hidden','true'); listToggle.setAttribute('aria-expanded','false'); listToggle.focus(); } -/* small helper: wait one animation frame */ -function tick() { - return new Promise(resolve => requestAnimationFrame(resolve)); -} - -/* ---------------- 初期化 ---------------- */ +/* ---------- init ---------- */ async function init() { - // create Panzoom instance applied to the entire map element + // Panzoom on map panzoomInstance = Panzoom(map, { maxScale: 4, minScale: 1, @@ -385,29 +354,18 @@ contain: 'outside' }); - // wheel zoom using wrapper (so page scroll works when appropriate) + // wheel zoom using wrapper (so page scroll possible when not zooming) wrapper.addEventListener('wheel', (ev) => { - // if ctrlKey (desktop pinch) or user wheel, zoom panzoomInstance.zoomWithWheel(ev); - // update touch action after wheel requestAnimationFrame(updateWrapperTouchAction); }, { passive: false }); - // pointer tracking to know multi-touch - map.addEventListener('pointerdown', (ev) => { - activePointers.add(ev.pointerId); - updateWrapperTouchAction(); - }); - map.addEventListener('pointerup', (ev) => { - activePointers.delete(ev.pointerId); - updateWrapperTouchAction(); - }); - map.addEventListener('pointercancel', (ev) => { - activePointers.delete(ev.pointerId); - updateWrapperTouchAction(); - }); + // pointer tracking (to detect multi-touch) + map.addEventListener('pointerdown', (ev) => { activePointers.add(ev.pointerId); updateWrapperTouchAction(); }); + map.addEventListener('pointerup', (ev) => { activePointers.delete(ev.pointerId); updateWrapperTouchAction(); }); + map.addEventListener('pointercancel', (ev) => { activePointers.delete(ev.pointerId); updateWrapperTouchAction(); }); - // pointermove -> schedule checking of transform (for pinch zoom in progress) + // pointermove scheduling let rafId = null; function scheduleUpdate() { if (rafId) return; @@ -417,31 +375,28 @@ map.addEventListener('pointerup', scheduleUpdate); map.addEventListener('pointercancel', scheduleUpdate); - // tab buttons + // tabs tabButtons.forEach(btn => btn.addEventListener('click', () => { const floor = Number(btn.dataset.floor); if (!Number.isNaN(floor)) loadFloor(floor); })); - // global tap closes tooltips (unless tapping inside map/pin/tooltip/list) + // global pointerdown closes tooltips (unless inside interactive areas) document.addEventListener('pointerdown', (ev) => { - const target = ev.target; - if (target.closest('.pin') || target.closest('.tooltip') || target.closest('.list-panel') || target.closest('.list-toggle')) { - return; - } + const t = ev.target; + if (t.closest('.pin') || t.closest('.tooltip') || t.closest('.list-panel') || t.closest('.list-toggle')) return; closeAllTooltips(); }); - // list toggle handlers + // list handlers listToggle.addEventListener('click', openList); closeListBtn.addEventListener('click', closeList); - // resize handler — on resize, reset pan & recalc base size & re-place pins + // resize: reset pan, remeasure base size and re-place pins let resizeTimer = null; window.addEventListener('resize', () => { if (resizeTimer) clearTimeout(resizeTimer); resizeTimer = setTimeout(async () => { - // reset pan to avoid transform interfering with measurements if (panzoomInstance && typeof panzoomInstance.reset === 'function') { panzoomInstance.reset(); try { panzoomInstance.pan(0,0); } catch(e) {} @@ -455,10 +410,10 @@ }, 220); }); - // initial load: 1F + // initial load (1F) img.src = IMAGE_PATH[1]; await waitImageLoad(img); - // ensure pan is reset and measure base + if (panzoomInstance && typeof panzoomInstance.reset === 'function') { panzoomInstance.reset(); try { panzoomInstance.pan(0,0); } catch(e) {} @@ -471,9 +426,7 @@ currentData = await loadCSV(CSV_PATH[1]); placePins(currentData); - // final touch-action update updateWrapperTouchAction(); } -// run document.addEventListener('DOMContentLoaded', init);