/* ============================================================ bnr_v3_ui.js — 本番UI配線(index.php) - 上枠(#down):下り → 上り を1枠でまとめて測定・両方表示 - 下枠(#up) :応答速度(レイテンシ)を約15秒 連続測定し、グラフ表示(ストップ可) - 記録:flash_post_v2.cgi(v3形式)へ。速度=DIR=down/up、レイテンシ=DIR=ping。 依存:jQuery, bnr_speedtest_v3.js(本番エンジンを変更せず利用) ============================================================ */ $(function () { // ヘッダのハンバーガーメニュー開閉(旧 common100069.js から移植・engine有無に関係なく動かす) $('header #nav .btn img').on('click', function () { $('header nav').animate({ right: '0' }, 300, 'swing'); }); $('header nav .close').on('click', function () { $('header nav').animate({ right: '-100%' }, 300, 'swing'); }); if (typeof BnrSpeedV3 === 'undefined') { return; } var CFG = BnrSpeedV3.CFG; var VERSION = 'v3.0'; var lineinfo = '', clientInfo = null; try { fetch('./cookie_v2.php', { cache: 'no-store' }).then(function (r) { return r.text(); }).then(function (t) { lineinfo = t || ''; }).catch(function () {}); } catch (e) {} try { fetch('./whoami.php', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (j) { clientInfo = j; }).catch(function () {}); } catch (e) {} function fmt(m) { return (m >= 100 ? m.toFixed(0) : m.toFixed(1)); } function fmtMs(m) { return Math.round(m); } // レイテンシは整数msに統一(小数の有無で幅が変わらないように) function fw(c, em) { return '' + c + ''; } function msCell(n) { return fw(n == null ? '--' : Math.round(n), 2.6) + ' ms'; } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function bust(u) { return u + (u.indexOf('?') < 0 ? '?' : '&') + 't=' + Date.now() + '_' + Math.random().toString(36).slice(2); } function xBtn(text) { var href = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text); return '𝕏 ポスト'; } var dtStamp = ''; function nowStr() { var n = new Date(), w = ['日','月','火','水','木','金','土'][n.getDay()]; function z(x){ return x < 10 ? '0' + x : x; } return n.getFullYear()+'年'+z(n.getMonth()+1)+'月'+z(n.getDate())+'日('+w+') '+z(n.getHours())+'時'+z(n.getMinutes())+'分'+z(n.getSeconds())+'秒'; } function head(suffix) { var h = '------ BNRスピードテスト ' + (suffix ? suffix + ' ' : '') + '------
'; h += '測定サイト: https://www.musen-lan.com/speed/ Ver ' + VERSION + '
'; h += '測定日時: ' + (dtStamp || nowStr()) + '
'; if (lineinfo) { h += '回線/ISP/地域: ' + lineinfo + '
'; } if (clientInfo && clientInfo.ip) { h += 'IPアドレス: ' + esc(clientInfo.ip) + '(' + esc(clientInfo.family) + ')
'; if (clientInfo.host) { h += 'ホスト名: ' + esc(clientInfo.host) + '
'; } } h += '--------------------------------------------------
'; return h; } /* ===== 記録(flash_post_v2.cgi / v3形式)===== */ function record(dir, mbps) { if (!(mbps > 0)) { return; } var kbps = Math.round(mbps * 1000 * 100) / 100; try { var x = new XMLHttpRequest(); x.open('POST', './flash_post_v2.cgi'); x.send('ACTION=REGIST&VERSION=' + VERSION + '&DIR=' + dir + '&SPEED=' + kbps); } catch (e) {} } var MINSAMP = 5; // 有効サンプルがこれ未満(途中ストップ等)なら記録しない function recordLat(st, total) { if (!st || !st.n || st.n < MINSAMP) { return; } var med = Math.round(st.med * 10) / 10, jit = Math.round(st.jit * 10) / 10, loss = st.loss || 0; try { var x = new XMLHttpRequest(); x.open('POST', './flash_post_v2.cgi'); x.send('ACTION=REGIST&VERSION=' + VERSION + '&DIR=ping&MED=' + med + '&JIT=' + jit + '&LOSS=' + loss + '&N=' + total); } catch (e) {} } /* ===== 上枠:下り+上り ===== */ var sp = { down: { prog: 0, live: 0, final: null }, up: { prog: 0, live: 0, final: null } }; function speedLine(label, color, s) { var bar = '
'; var num; if (s.final != null) { num = '' + label + ' ' + fw(fmt(s.final), 3.4) + ' Mbps'; } else if (s.live > 0) { num = '' + label + ' 測定中: ' + fw(fmt(s.live), 3.4) + ' Mbps'; } else { num = '' + label + ' 測定待ち…'; } return num + bar; } function renderSpeed() { var html = head('通信速度の測定'); html += speedLine('▼ 下り(ダウンロード)', '#2f6db0', sp.down); html += speedLine('▲ 上り(アップロード)', '#d07f2c', sp.up); if (sp.down.final != null && sp.up.final != null) { html += xBtn('BNRスピードテスト(インターネット通信速度)\n測定日時: ' + dtStamp + '\n下り: ' + fmt(sp.down.final) + 'Mbps / 上り: ' + fmt(sp.up.final) + 'Mbps\nhttps://www.musen-lan.com/speed/'); } $('#down').find('[name=result]').html(html); } async function doBoth() { dtStamp = nowStr(); sp = { down: { prog: 0, live: 0, final: null }, up: { prog: 0, live: 0, final: null } }; renderSpeed(); var d = await BnrSpeedV3.runDownload(function (p, bps) { sp.down.prog = p; sp.down.live = bps / 1e6; renderSpeed(); }); sp.down.prog = 100; sp.down.live = 0; sp.down.final = d.ok ? d.mbps : 0; renderSpeed(); if (d.ok) { record('down', d.mbps); } var u = await BnrSpeedV3.runUpload(function (p, bps) { sp.up.prog = p; sp.up.live = bps / 1e6; renderSpeed(); }); sp.up.prog = 100; sp.up.live = 0; sp.up.final = u.ok ? u.mbps : 0; renderSpeed(); if (u.ok) { record('up', u.mbps); } } /* ===== 下枠:レイテンシ連続グラフ(約15秒・ストップ可)===== */ var DUR = 15000, INTERVAL = 300; var samples = [], monStop = false, gC = null, gX = null, latDone = false, latLabel = ''; function gInit() { gC = document.getElementById('lat-graph'); if (!gC) return; gC.style.display = 'block'; gX = gC.getContext('2d'); gC.width = gC.clientWidth || 520; gC.height = 180; } function statsOf() { var r = samples.filter(function (s) { return s.rtt != null; }).map(function (s) { return s.rtt; }); var loss = samples.length - r.length; if (!r.length) return { n: 0, loss: loss }; var s = r.slice().sort(function (a, b) { return a - b; }); var sum = 0; r.forEach(function (v) { sum += v; }); var j = 0; for (var i = 1; i < r.length; i++) j += Math.abs(r[i] - r[i - 1]); return { n: r.length, loss: loss, min: s[0], max: s[s.length - 1], med: s[Math.floor(s.length / 2)], avg: sum / r.length, jit: r.length > 1 ? j / (r.length - 1) : 0 }; } function gDraw() { if (!gX) return; var W = gC.width, H = gC.height; gX.clearRect(0, 0, W, H); var maxR = 10; samples.forEach(function (s) { if (s.rtt != null && s.rtt > maxR) maxR = s.rtt; }); var ymax = Math.ceil(maxR * 1.25 / 10) * 10; if (ymax < 30) ymax = 30; var padL = 24, padR = 24, padT = 18, padB = 22, plotW = W - padL - padR, plotH = H - padT - padB; function xOf(t) { return padL + (DUR ? (t / DUR) : 0) * plotW; } function yOf(r) { return padT + plotH - (Math.min(r, ymax) / ymax) * plotH; } // 品質帯(背景・薄色):〜30ms 良好(緑)/〜80ms 普通(黄)/80ms〜 やや遅い(赤) function band(lo, hi, col) { var y1 = yOf(Math.min(hi, ymax)), y2 = yOf(lo); gX.fillStyle = col; gX.fillRect(padL, y1, plotW, y2 - y1); } band(0, 30, 'rgba(46,204,113,0.42)'); if (ymax > 30) band(30, 80, 'rgba(241,196,15,0.42)'); if (ymax > 80) band(80, ymax, 'rgba(231,76,60,0.44)'); // Y グリッド+ラベル gX.strokeStyle = '#2a3a5c'; gX.fillStyle = '#9fb0cc'; gX.font = '10px sans-serif'; gX.lineWidth = 1; gX.textAlign = 'right'; var gstep = ymax <= 60 ? 5 : (ymax <= 120 ? 10 : (ymax <= 300 ? 25 : (ymax <= 600 ? 50 : 100))); for (var yv = 0; yv <= ymax + 0.5; yv += gstep) { var y = padT + plotH - (yv / ymax) * plotH; gX.beginPath(); gX.moveTo(padL, y); gX.lineTo(W - padR, y); gX.stroke(); gX.fillText(yv, padL - 4, y + 3); } gX.textAlign = 'right'; gX.fillStyle = '#9bb0d8'; gX.fillText('ms', padL - 4, 9); // 縦軸の単位(最上部・数値の上に右寄せ) // X 目盛(秒) var durSec = Math.round(DUR / 1000), step = durSec >= 20 ? 10 : 5; gX.textAlign = 'center'; gX.strokeStyle = '#1c2742'; gX.fillStyle = '#9fb0cc'; for (var sx = step; sx <= durSec; sx += step) { var x = xOf(sx * 1000); gX.beginPath(); gX.moveTo(x, padT); gX.lineTo(x, padT + plotH); gX.stroke(); gX.fillText(sx + '秒', x, H - 6); } // 平均線 var st = statsOf(); if (st.n) { var ya = yOf(st.avg); gX.strokeStyle = '#f0c040'; gX.lineWidth = 1.3; gX.setLineDash([4, 4]); gX.beginPath(); gX.moveTo(padL, ya); gX.lineTo(W - padR, ya); gX.stroke(); gX.setLineDash([]); } // 折れ線(明るい緑・暗背景で目立つ) gX.strokeStyle = '#ffffff'; gX.lineWidth = 1.8; gX.beginPath(); var started = false; samples.forEach(function (s) { if (s.rtt == null) { started = false; gX.save(); gX.fillStyle = '#ff5252'; gX.fillRect(xOf(s.t) - 1.5, padT + plotH - 4, 3, 4); gX.restore(); return; } var x = xOf(s.t), y = yOf(s.rtt); if (!started) { gX.moveTo(x, y); started = true; } else gX.lineTo(x, y); }); gX.stroke(); } function gStats() { var st = statsOf(); var last = samples.length ? samples[samples.length - 1].rtt : null; var html = head('レイテンシの測定') + '応答速度(レイテンシ) ' + (st.n ? ('' + fmtMs(st.med) + ' ms 中央値') : '測定待ち…') + (latLabel ? ' (計測先: ' + esc(latLabel) + ')' : '') + '
' + '現在 ' + msCell(last) + ' / 最小 ' + msCell(st.n ? st.min : null) + ' / 最大 ' + msCell(st.n ? st.max : null) + ' / 平均 ' + msCell(st.n ? st.avg : null) + '
Jitter ' + msCell(st.n ? st.jit : null) + ' / 欠損 ' + (st.loss || 0) + ' / ' + (samples.length || 0) + '回
'; if (latDone && st.n) { html += xBtn('BNRスピードテスト(応答速度/レイテンシ)\n測定日時: ' + dtStamp + '\n中央値 ' + fmtMs(st.med) + 'ms / 最小 ' + fmtMs(st.min) + 'ms / 最大 ' + fmtMs(st.max) + 'ms / 平均 ' + fmtMs(st.avg) + 'ms' + '\nJitter ' + fmtMs(st.jit) + 'ms / 欠損 ' + (st.loss || 0) + ' / ' + (samples.length || 0) + '回' + '\nhttps://www.musen-lan.com/speed/'); } $('#up').find('[name=result]').html(html); } function bestUrl(p) { var fb = (CFG.pingTargets && CFG.pingTargets[0]) ? CFG.pingTargets[0].url : null; if (p && p.best && CFG.pingTargets) { for (var i = 0; i < CFG.pingTargets.length; i++) { if (CFG.pingTargets[i].label === p.best.label) return CFG.pingTargets[i].url; } } return fb; } async function doLatencyGraph() { gInit(); samples = []; monStop = false; latDone = false; dtStamp = nowStr(); gDraw(); gStats(); var p = await BnrSpeedV3.runPing(); // まず最速拠点を選ぶ var url = bestUrl(p); latLabel = (p && p.best) ? p.best.label : ''; if (!url) { $('#up').find('[name=result]').html('計測先が見つかりませんでした。'); return; } var start = performance.now(), timeout = CFG.pingTimeout || 1500; return new Promise(function (resolve) { function tick() { if (monStop || performance.now() - start >= DUR) { latDone = true; gDraw(); gStats(); recordLat(statsOf(), samples.length); resolve(); return; } var t0 = performance.now(), ctrl = new AbortController(); var to = setTimeout(function () { try { ctrl.abort(); } catch (e) {} }, timeout); fetch(bust(url), { cache: 'no-store', signal: ctrl.signal }).then(function (r) { clearTimeout(to); var rtt = performance.now() - t0; if (r && typeof r.arrayBuffer === 'function') { r.arrayBuffer().catch(function () {}); } rec(rtt); }).catch(function () { clearTimeout(to); rec(null); }); } function rec(rtt) { samples.push({ t: performance.now() - start, rtt: rtt }); gDraw(); gStats(); setTimeout(tick, INTERVAL); } tick(); }); } /* ===== ボタン配線 ===== */ // 上枠:下り+上り $('#down-start').off('click').on('click', async function () { var $b = $(this); $b.attr('disabled', true).text('測定中…'); $('#up-start').attr('disabled', true); await doBoth(); $b.attr('disabled', null).text('測定開始'); $('#up-start').attr('disabled', null); }); // 下枠:レイテンシ連続グラフ $('#up-start').off('click').on('click', async function () { var $b = $(this); $b.attr('disabled', true).text('測定中…'); $('#up-stop').attr('disabled', null); $('#down-start').attr('disabled', true); await doLatencyGraph(); $b.attr('disabled', null).text('測定開始'); $('#up-stop').attr('disabled', true); $('#down-start').attr('disabled', null); }); $('#up-stop').off('click').on('click', function () { monStop = true; }); // 初期表示:結果テキストは空白のまま、グラフ枠だけ先に描く gInit(); gDraw(); });