/* ============================================================
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();
});