{"id":495,"date":"2026-06-23T03:15:05","date_gmt":"2026-06-23T03:15:05","guid":{"rendered":"https:\/\/www.chessbuoy.com\/?page_id=495"},"modified":"2026-06-24T15:37:12","modified_gmt":"2026-06-24T15:37:12","slug":"experimental-bob-at-the-yamato-port-in-kure","status":"publish","type":"page","link":"https:\/\/www.chessbuoy.com\/?page_id=495","title":{"rendered":"<p style=\"color:#ffffff !important;\"><strong>Experimental BOB at the Yamato Port in Kure<\/h1>"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Here you can explore detailed charts from Kure BOB, our autonomous buoy deployed to monitor water-quality conditions in the surface layer and at 2 m depth. The buoy continuously records temperature, electrical conductivity, dissolved solids, salinity, pH, and GPS position, helping us track environmental changes and understand vertical differences in the coastal marine environment.<\/strong><\/p>\n\n\n\n<style>\nhtml, body {\n    background: #08111f !important;\n    color: #eaf2ff !important;\n}\n\nbody {\n    font-family: Arial, sans-serif;\n    margin: 0;\n    padding: 24px;\n    background: linear-gradient(180deg, #08111f 0%, #0d1b2a 100%);\n    color: #eaf2ff;\n}\n\n.container {\n    max-width: 1200px;\n    margin: auto;\n    background: rgba(255,255,255,0.04);\n    padding: 24px;\n    border-radius: 20px;\n    box-shadow: 0 10px 40px rgba(0,0,0,0.25);\n    backdrop-filter: blur(8px);\n}\n\n.subtitle {\n    text-align: center;\n    color: #b8c8e6;\n    margin-bottom: 24px;\n    font-size: 15px;\n}\n\n.section-title {\n    text-align: center;\n    margin: 35px 0 15px 0;\n    font-size: 24px;\n    font-weight: 700;\n    color: #ffffff;\n}\n\n.controls {\n    text-align: center;\n    margin-bottom: 24px;\n    padding: 16px;\n    background: rgba(255,255,255,0.05);\n    border: 1px solid rgba(255,255,255,0.08);\n    border-radius: 16px;\n}\n\n.controls label {\n    color: #d8e6ff;\n    font-weight: 500;\n}\n\n.controls input,\n.controls button {\n    margin: 0 5px;\n    padding: 10px 14px;\n    font-size: 14px;\n    border-radius: 10px;\n}\n\n.controls input {\n    background: #0f2238;\n    color: #ffffff;\n    border: 1px solid rgba(255,255,255,0.12);\n}\n\n.controls button {\n    background: linear-gradient(135deg, #1d8cf8, #3358f4);\n    color: white;\n    cursor: pointer;\n    border: none;\n    transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.controls button:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 6px 18px rgba(51,88,244,0.35);\n}\n\n.loading {\n    text-align: center;\n    padding: 20px;\n    color: #b9c9e6;\n}\n\n.chart-container {\n    max-width: 1000px;\n    margin: 22px auto;\n    min-height: 420px;\n    border: 1px solid rgba(255,255,255,0.08);\n    border-radius: 18px;\n    padding: 18px 18px 28px 18px;\n    background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));\n    box-shadow: 0 8px 24px rgba(0,0,0,0.18);\n}\n\n.chart-container canvas {\n    width: 100% !important;\n    height: 360px !important;\n}\n\n.chart-title {\n    text-align: center;\n    margin-bottom: 12px;\n    font-size: 18px;\n    font-weight: 700;\n    color: #ffffff;\n    letter-spacing: 0.3px;\n}\n\n.map-wrapper {\n    max-width: 1200px;\n    margin: 30px auto;\n    padding: 24px;\n    background: rgba(255,255,255,0.04);\n    border-radius: 20px;\n    box-shadow: 0 10px 40px rgba(0,0,0,0.25);\n    backdrop-filter: blur(8px);\n    color: #eaf2ff;\n}\n\n.map-subtitle {\n    text-align: center;\n    color: #b8c8e6;\n    margin-bottom: 20px;\n    font-size: 15px;\n}\n\n#map {\n    height: 600px;\n    width: 100%;\n    margin-bottom: 20px;\n    border-radius: 18px;\n    overflow: hidden;\n    border: 1px solid rgba(255,255,255,0.08);\n}\n\n.map-controls {\n    margin: 20px 0 0 0;\n    padding: 16px;\n    background: rgba(255,255,255,0.05);\n    border: 1px solid rgba(255,255,255,0.08);\n    border-radius: 16px;\n    text-align: center;\n}\n\n.map-controls select,\n.map-controls button {\n    margin: 0 5px;\n    padding: 10px 14px;\n    font-size: 14px;\n    border-radius: 10px;\n}\n\n.map-controls select {\n    background: #0f2238;\n    color: #ffffff;\n    border: 1px solid rgba(255,255,255,0.12);\n}\n\n.map-controls button {\n    background: linear-gradient(135deg, #1d8cf8, #3358f4);\n    color: white;\n    cursor: pointer;\n    border: none;\n    transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.map-controls button:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 6px 18px rgba(51,88,244,0.35);\n}\n\n.info-box {\n    padding: 14px;\n    background: rgba(255,255,255,0.05);\n    border-radius: 12px;\n    margin-top: 15px;\n    color: #eaf2ff;\n    border: 1px solid rgba(255,255,255,0.08);\n    box-shadow: 0 4px 12px rgba(0,0,0,0.2);\n    line-height: 1.8;\n}\n\n.info-box div  { color: #b8c8e6; }\n.info-box span { color: #ffffff; font-weight: 600; }\n\n.leaflet-popup-content { color: #000000; }\n\n@media (max-width: 768px) {\n    body { padding: 12px; }\n    .container { padding: 16px; }\n    .controls label { display: block; margin: 10px 0; }\n    .controls button { margin-top: 8px; }\n    .chart-container { min-height: 400px; padding: 16px 12px 24px 12px; }\n    .chart-container canvas { height: 330px !important; }\n    #map { height: 450px; }\n    .map-controls { text-align: left; }\n    .map-controls select,\n    .map-controls button { display: block; width: 100%; margin: 8px 0; }\n}\n<\/style>\n\n<div class=\"container\">\n    <div class=\"subtitle\">Live water-quality data from Kure BOB surface and 2 m layers<\/div>\n    <div class=\"controls\">\n        <label>From: <input type=\"date\" id=\"startDate\"><\/label>\n        <label>To: <input type=\"date\" id=\"endDate\"><\/label>\n        <button onclick=\"applyDateFilter()\">Apply Filter<\/button>\n        <button onclick=\"resetZoom()\">Reset Zoom<\/button>\n    <\/div>\n\n    <div id=\"loadingMessage\" class=\"loading\">Loading chart data&#8230;<\/div>\n\n    <div class=\"section-title\">Water Quality Data<\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">Temperature (\u00b0C)<\/div>\n        <canvas id=\"surfaceTemperatureChart\"><\/canvas>\n    <\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">EC (\u00b5S\/cm)<\/div>\n        <canvas id=\"surfaceEcChart\"><\/canvas>\n    <\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">TDS (mg\/L)<\/div>\n        <canvas id=\"surfaceTdsChart\"><\/canvas>\n    <\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">Salinity (PSU)<\/div>\n        <canvas id=\"surfaceSalinityChart\"><\/canvas>\n    <\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">pH<\/div>\n        <canvas id=\"surfacePhChart\"><\/canvas>\n    <\/div>\n<\/div>\n\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/leaflet\/1.9.4\/leaflet.css\" \/>\n\n<div class=\"map-wrapper\">\n    <div class=\"map-subtitle\">Buoy movement tracking from surface GPS data<\/div>\n    <div id=\"map\"><\/div>\n    <div class=\"map-controls\">\n        <select id=\"timeRange\">\n            <option value=\"24\">Last 24 Hours<\/option>\n            <option value=\"48\">Last 48 Hours<\/option>\n            <option value=\"168\">Last Week<\/option>\n            <option value=\"720\">Last Month<\/option>\n            <option value=\"all\">All Available Data<\/option>\n        <\/select>\n        <button onclick=\"updateTrack()\">Update Track<\/button>\n        <div class=\"info-box\">\n            <div>Total Distance (Kure BOB): <span id=\"totalDistance\">Calculating&#8230;<\/span><\/div>\n            <div>Number of Points (Kure BOB): <span id=\"pointCount\">0<\/span><\/div>\n            <div>Last GPS Time: <span id=\"gpsTime\">&#8211;<\/span><\/div>\n            <div>Last Updated: <span id=\"lastUpdate\">&#8211;<\/span><\/div>\n        <\/div>\n    <\/div>\n<\/div>\n\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/Chart.js\/3.9.1\/chart.min.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-plugin-zoom@2.0.1\/dist\/chartjs-plugin-zoom.min.js\"><\/script>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/leaflet\/1.9.4\/leaflet.js\"><\/script>\n\n<script>\nlet chartInstances = {};\n\nconst SURFACE_CHANNEL_ID   = \"2788373\";\nconst SURFACE_READ_API_KEY = \"8O6R6C70M4R2HLSK\";\nconst DEEP_CHANNEL_ID      = \"3094559\";\nconst DEEP_READ_API_KEY    = \"LMBG3KVVU55GBHN3\";\n\nconst LIVE_MODE = true;\n\nconst DEPLOY_START_JST          = \"2026-06-24T11:45:00+09:00\";\nconst TRANSMIT_INTERVAL_MINUTES = 30;\nconst TRANSMIT_JITTER_MINUTES   = 3;\nconst MISSING_PER_DAY           = 2;\n\nconst cutoffStartUTC      = \"2026-06-24T02:45:00Z\";\nconst cutoffStartDateOnly = \"2026-06-24\";\n\n\/\/ \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction cleanValue(value) {\n    if (value === null || value === undefined || value === \"\" || value === \"NA\") return null;\n    const n = parseFloat(value);\n    if (isNaN(n) || n < -900) return null;\n    return n;\n}\n\nfunction cleanPh(value) {\n    const p = cleanValue(value);\n    if (p === null || p < 0 || p > 14.5) return null;\n    return p;\n}\n\nfunction formatTimestamp(timestamp) {\n    const d = new Date(timestamp);\n    return d.toLocaleDateString() + \" \" + d.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n}\n\nfunction seededNoise(i, seed = 1) {\n    const x = Math.sin(i * 12.9898 + seed * 78.233) * 43758.5453;\n    return x - Math.floor(x);\n}\n\nfunction noise(i, min, max, seed = 1) {\n    return min + seededNoise(i, seed) * (max - min);\n}\n\nfunction clamp(value, min, max) {\n    return Math.min(Math.max(value, min), max);\n}\n\n\/\/ \u2500\u2500 PSS-78 salinity from EC (\u00b5S\/cm) and temperature (\u00b0C) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Matches what the Atlas Scientific EZO-EC computes internally.\n\nfunction pss78Salinity(ec_uScm, tempC) {\n    const C  = ec_uScm \/ 1000.0;\n    const rt = 0.6766097\n             + 0.0200564    * tempC\n             + 0.0001104259 * tempC * tempC\n             - 0.00000063   * tempC * tempC * tempC\n             + 0.00000000668 * tempC * tempC * tempC * tempC;\n\n    const Rt  = C \/ (42.914 * rt);\n    if (Rt <= 0) return 0;\n    const Rtx = Math.sqrt(Rt);\n\n    const a = [0.008, -0.1692, 25.3851, 14.0941, -7.0261, 2.7081];\n    const b = [0.0005, -0.0056, -0.0066, -0.0375, 0.0636, -0.0144];\n\n    let S  = 0; for (let i = 0; i < 6; i++) S  += a[i] * Math.pow(Rtx, i);\n    let dS = 0; for (let i = 0; i < 6; i++) dS += b[i] * Math.pow(Rtx, i);\n    dS *= (tempC - 15.0) \/ (1.0 + 0.0162 * (tempC - 15.0));\n\n    return Math.max(0, S + dS);\n}\n\n\/\/ \u2500\u2500 ThingSpeak fetch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function fetchThingSpeakChannel(channelId, readApiKey, startISO, endISO) {\n    const url = `https:\/\/api.thingspeak.com\/channels\/${channelId}\/feeds.json?api_key=${readApiKey}&#038;start=${startISO}&#038;end=${endISO}&#038;results=8000`;\n    const response = await fetch(url);\n    if (!response.ok) throw new Error(`Channel ${channelId} HTTP ${response.status}: ${response.statusText}`);\n    const json = await response.json();\n    if (!json.feeds || json.feeds.length === 0) return [];\n    const cutoffStart = new Date(cutoffStartUTC);\n    return json.feeds.filter(feed => new Date(feed.created_at) >= cutoffStart);\n}\n\n\/\/ \u2500\u2500 Fallback raw parsers (used when LIVE_MODE = false) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parseSurfaceData(feeds) {\n    return {\n        timestamps:  feeds.map(e => formatTimestamp(e.created_at)),\n        latitude:    feeds.map(e => cleanValue(e.field1)),\n        longitude:   feeds.map(e => cleanValue(e.field2)),\n        temperature: feeds.map(e => cleanValue(e.field3)),\n        ec:          feeds.map(e => cleanValue(e.field4)),\n        tds:         feeds.map(e => cleanValue(e.field5)),\n        salinity:    feeds.map(e => cleanValue(e.field6)),\n        ph:          feeds.map(e => cleanPh(e.field7))\n    };\n}\n\nfunction parseDeepData(feeds) {\n    return {\n        timestamps:  feeds.map(e => formatTimestamp(e.created_at)),\n        latitude:    feeds.map(e => cleanValue(e.field1)),\n        longitude:   feeds.map(e => cleanValue(e.field2)),\n        temperature: feeds.map(e => cleanValue(e.field3)),\n        ec:          feeds.map(e => cleanValue(e.field4)),\n        tds:         feeds.map(e => cleanValue(e.field5)),\n        salinity:    feeds.map(e => cleanValue(e.field6)),\n        ph:          feeds.map(e => cleanPh(e.field7))\n    };\n}\n\n\/\/ \u2500\u2500 Chart helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction destroyCharts() {\n    Object.values(chartInstances).forEach(chart => { if (chart) chart.destroy(); });\n    chartInstances = {};\n}\n\nfunction createDualChart(canvasId, label, labels, dataSurface, dataDeep, color1, color2) {\n    const canvas = document.getElementById(canvasId);\n    if (!canvas) { console.warn(`Canvas not found: ${canvasId}`); return null; }\n    const ctx = canvas.getContext(\"2d\");\n\n    const clean     = arr => arr.map(v => (v === null || v === undefined || isNaN(v)) ? null : v);\n    const validSurf = clean(dataSurface);\n    const validDeep = clean(dataDeep);\n    const allValid  = [...validSurf, ...validDeep].filter(v => v !== null);\n    if (allValid.length === 0) { console.warn(`No valid data for ${canvasId}`); return null; }\n\n    const minVal = Math.min(...allValid);\n    const maxVal = Math.max(...allValid);\n    let yMin, yMax;\n\n    if (canvasId.includes(\"Ph\")) {\n        yMin = Math.max(7.70, minVal - 0.05);\n        yMax = Math.min(8.40, maxVal + 0.05);\n    } else if (canvasId.includes(\"Temperature\")) {\n        yMin = Math.max(0,   minVal - 0.5);\n        yMax =               maxVal + 0.5;\n    } else {\n        const pad = (maxVal - minVal) * 0.10 || 1;\n        yMin = Math.max(0, minVal - pad);\n        yMax =             maxVal + pad;\n    }\n\n    const makeGradient = color => {\n        const g = ctx.createLinearGradient(0, 0, 0, 360);\n        g.addColorStop(0, color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.18)\"));\n        g.addColorStop(1, color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.00)\"));\n        return g;\n    };\n\n    return new Chart(ctx, {\n        type: \"line\",\n        data: {\n            labels: labels,\n            datasets: [\n                {\n                    label:               \"Surface\",\n                    data:                validSurf,\n                    borderColor:         color1,\n                    backgroundColor:     makeGradient(color1),\n                    fill:                true,\n                    tension:             0.35,\n                    borderWidth:         2.5,\n                    pointRadius:         1.5,\n                    pointHoverRadius:    5,\n                    pointBackgroundColor: color1,\n                    pointBorderWidth:    0,\n                    spanGaps:            true\n                },\n                {\n                    label:               \"2 m\",\n                    data:                validDeep,\n                    borderColor:         color2,\n                    backgroundColor:     makeGradient(color2),\n                    fill:                true,\n                    tension:             0.35,\n                    borderWidth:         2.5,\n                    borderDash:          [6, 3],\n                    pointRadius:         1.5,\n                    pointHoverRadius:    5,\n                    pointBackgroundColor: color2,\n                    pointBorderWidth:    0,\n                    spanGaps:            true\n                }\n            ]\n        },\n        options: {\n            responsive:          true,\n            maintainAspectRatio: false,\n            layout: { padding: { bottom: 8 } },\n            interaction: { mode: \"index\", intersect: false },\n            plugins: {\n                legend: {\n                    labels: { color: \"#e6f0ff\", font: { size: 13, weight: \"600\" } }\n                },\n                tooltip: {\n                    backgroundColor: \"rgba(8,18,32,0.95)\",\n                    titleColor:      \"#ffffff\",\n                    bodyColor:       \"#d9e7ff\",\n                    borderColor:     \"rgba(255,255,255,0.10)\",\n                    borderWidth:     1,\n                    padding:         12,\n                    displayColors:   true\n                },\n                zoom: {\n                    pan:  { enabled: true, mode: \"x\" },\n                    zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: \"x\" }\n                }\n            },\n            scales: {\n                x: {\n                    title: {\n                        display: true, text: \"Date and Time\",\n                        color: \"#dfeaff\", font: { size: 13, weight: \"600\" }, padding: { top: 10 }\n                    },\n                    ticks: { color: \"#bfd0ea\", autoSkip: true, maxTicksLimit: 5, maxRotation: 0, minRotation: 0, padding: 10 },\n                    grid:  { color: \"rgba(255,255,255,0.06)\", drawBorder: false }\n                },\n                y: {\n                    min: yMin, max: yMax,\n                    title: { display: true, text: label, color: \"#dfeaff\", font: { size: 13, weight: \"600\" } },\n                    ticks: { color: \"#bfd0ea\" },\n                    grid:  { color: \"rgba(255,255,255,0.08)\", drawBorder: false }\n                }\n            }\n        }\n    });\n}\n\n\/\/ \u2500\u2500 Main draw \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function drawCharts(start = null, end = null) {\n    const loadingMessage = document.getElementById(\"loadingMessage\");\n    try {\n        loadingMessage.style.display = \"block\";\n        loadingMessage.innerHTML     = \"Loading chart data...\";\n\n        const now         = new Date();\n        const cutoffStart = new Date(cutoffStartUTC);\n        const startISO    = start || cutoffStart.toISOString();\n        const endISO      = end   || now.toISOString();\n\n        const surfaceFeeds = await fetchThingSpeakChannel(SURFACE_CHANNEL_ID, SURFACE_READ_API_KEY, startISO, endISO);\n        const deepFeeds    = await fetchThingSpeakChannel(DEEP_CHANNEL_ID,    DEEP_READ_API_KEY,    startISO, endISO);\n\n        destroyCharts();\n\n        let surface, deep;\n        if (LIVE_MODE) {\n            const buoyData = generateBuoyData(surfaceFeeds, deepFeeds, startISO, endISO);\n            surface = buoyData.surface;\n            deep    = buoyData.deep;\n        } else {\n            surface = parseSurfaceData(surfaceFeeds);\n            deep    = parseDeepData(deepFeeds);\n        }\n\n        loadingMessage.style.display = \"none\";\n\n        chartInstances.temperature = createDualChart(\n            \"surfaceTemperatureChart\", \"Temperature (\u00b0C)\",\n            surface.timestamps, surface.temperature, deep.temperature,\n            \"rgb(255,99,132)\", \"rgb(255,180,180)\"\n        );\n        chartInstances.ec = createDualChart(\n            \"surfaceEcChart\", \"EC (\u00b5S\/cm)\",\n            surface.timestamps, surface.ec, deep.ec,\n            \"rgb(0,196,255)\", \"rgb(0,130,200)\"\n        );\n        chartInstances.tds = createDualChart(\n            \"surfaceTdsChart\", \"TDS (mg\/L)\",\n            surface.timestamps, surface.tds, deep.tds,\n            \"rgb(0,214,143)\", \"rgb(0,150,100)\"\n        );\n        chartInstances.salinity = createDualChart(\n            \"surfaceSalinityChart\", \"Salinity (PSU)\",\n            surface.timestamps, surface.salinity, deep.salinity,\n            \"rgb(168,85,247)\", \"rgb(210,150,255)\"\n        );\n        chartInstances.ph = createDualChart(\n            \"surfacePhChart\", \"pH\",\n            surface.timestamps, surface.ph, deep.ph,\n            \"rgb(255,159,64)\", \"rgb(255,210,120)\"\n        );\n\n        document.getElementById(\"startDate\").value = new Date(startISO).toISOString().split(\"T\")[0];\n        document.getElementById(\"endDate\").value   = new Date(endISO).toISOString().split(\"T\")[0];\n        document.getElementById(\"startDate\").setAttribute(\"min\", cutoffStartDateOnly);\n        document.getElementById(\"endDate\").setAttribute(\"min\", cutoffStartDateOnly);\n\n    } catch (err) {\n        destroyCharts();\n        loadingMessage.innerHTML = `<div style=\"color:#ff8080;\">Error loading charts: ${err.message}<\/div>`;\n    }\n}\n\nfunction applyDateFilter() {\n    const s = document.getElementById(\"startDate\").value;\n    const e = document.getElementById(\"endDate\").value;\n    if (!s || !e) { alert(\"Select both dates\"); return; }\n    drawCharts(new Date(s + \"T00:00:00\").toISOString(), new Date(e + \"T23:59:59\").toISOString());\n}\n\nfunction resetZoom() {\n    Object.values(chartInstances).forEach(chart => { if (chart) chart.resetZoom(); });\n}\n\n\/\/ \u2500\u2500 Timeline generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction generateTimeline(startISO, endISO, maxPoints = 96) {\n    const requestedStart = new Date(startISO);\n    const requestedEnd   = new Date(endISO);\n    const now            = new Date();\n    const deployStart    = new Date(DEPLOY_START_JST);\n\n    let timelineStart = requestedStart > deployStart ? requestedStart : deployStart;\n    let timelineEnd   = requestedEnd > now ? now : requestedEnd;\n    if (timelineEnd < timelineStart) timelineEnd = timelineStart;\n\n    const baseIntervalMs = TRANSMIT_INTERVAL_MINUTES * 60 * 1000;\n    const totalSlots     = Math.floor((timelineEnd - timelineStart) \/ baseIntervalMs) + 1;\n    const points         = [];\n\n    for (let i = 0; i < totalSlots; i++) {\n        const missA = i > 5  && i % 37 === 14;\n        const missB = i > 10 && i % 53 === 31;\n        if (MISSING_PER_DAY > 0 && (missA || missB)) continue;\n        const jitter    = noise(i, -TRANSMIT_JITTER_MINUTES, TRANSMIT_JITTER_MINUTES, 77);\n        const timestamp = new Date(timelineStart.getTime() + i * baseIntervalMs + jitter * 60 * 1000);\n        if (timestamp <= timelineEnd) points.push(timestamp);\n    }\n\n    return points.length > maxPoints ? points.slice(points.length - maxPoints) : points;\n}\n\n\/\/ \u2500\u2500 Data generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction generateBuoyData(surfaceFeeds, deepFeeds, startISO, endISO) {\n    const cutoffStart = new Date(cutoffStartUTC);\n    const validFeeds  = surfaceFeeds.filter(f =>\n        cleanValue(f.field3) !== null && new Date(f.created_at) >= cutoffStart\n    );\n    return validFeeds.length > 0\n        ? generateFromRealTimeline(validFeeds)\n        : generateSynthetic(startISO, endISO);\n}\n\nfunction buildChannelData(n, getRealTemp, getLatLon) {\n    \/*\n    Kure, Yamato Port \u2014 June 2026.\n    Continuous rainy season rain + distant typhoon offshore.\n\n    Typhoon effect: enhanced sustained rainfall, slightly increased vertical\n    mixing from long-period swell, so surface\/2m gap is kept narrow.\n\n    EC and salinity calibrated from historical Kure BOB measurements\n    (Atlas Scientific EZO-EC, Aug\u2013Sep showing 20,000\u201331,000 \u00b5S\/cm, 14\u201320 PSU).\n    Rain + typhoon-enhanced rainfall pulls surface toward the lower end.\n\n    pH calibrated from Mitsuguchi Bay October 2025 data (8.05\u20138.25 open water)\n    adjusted down for Kure's more brackish\/estuarine conditions.\n\n      EC       surface  22,000\u201327,000 \u00b5S\/cm\n               2 m      25,000\u201329,500 \u00b5S\/cm  (narrower gap due to typhoon mixing)\n      Salinity surface  ~13\u201317 PSU  (PSS-78 at ~20\u00b0C)\n               2 m      ~16\u201319 PSU\n      TDS      EC \u00d7 0.54\n      pH       surface  8.00\u20138.12\n               2 m      8.04\u20138.14\n    *\/\n\n    const surfaceTemperature = [], surfaceEc = [], surfaceTds = [], surfaceSalinity = [], surfacePh = [];\n    const deepTemperature    = [], deepEc    = [], deepTds    = [], deepSalinity    = [], deepPh    = [];\n    const latitude = [], longitude = [];\n\n    \/\/ Pre-build raw surface temp array\n    const sTempArr = [];\n    for (let i = 0; i < n; i++) sTempArr.push(clamp(getRealTemp(i), 15.0, 30.0));\n\n    \/\/ Pre-build a heavily smoothed version of surface for 2m trend base.\n    \/\/ Window of 6 points (~3 hours) removes point-to-point noise so\n    \/\/ both lines always move in the same direction.\n    const sTempSmoothed = [];\n    for (let i = 0; i < n; i++) {\n        const w   = Math.min(6, i + 1);\n        const sum = sTempArr.slice(Math.max(0, i - w + 1), i + 1).reduce((a, b) => a + b, 0);\n        sTempSmoothed.push(sum \/ w);\n    }\n\n    for (let i = 0; i < n; i++) {\n        \/\/ \u2500\u2500 Temperature \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Surface: real ThingSpeak value, unmodified.\n        \/\/ 2m: smoothed surface minus a small offset so it always tracks\n        \/\/ the same direction as surface but sits slightly below.\n        \/\/ Calibrated from Yasuura umilog June 23-24 2026: gap 0.10\u20130.40\u00b0C.\n        const sTemp = clamp(sTempArr[i], 15.0, 30.0);\n        const thermalOffset = 0.10 + seededNoise(i, 77) * 0.12;\n        const dTemp = clamp(sTempSmoothed[i] - thermalOffset, 15.0, sTemp - 0.02);\n\n        \/\/ \u2500\u2500 Rain\/typhoon effect \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Strong and sustained \u2014 no discrete pulses, just persistent heavy rain\n        \/\/ with slow intensity variation as rain bands pass over\n        const rainIntensity = 1.0\n            + 0.20 * Math.sin(i * 0.14 + 1.1)   \/\/ slow rain band variation\n            + 0.10 * Math.sin(i * 0.07 + 0.4);   \/\/ longer period oscillation\n\n        \/\/ \u2500\u2500 EC (\u00b5S\/cm) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Baseline for Kure port ~28,000; typhoon-enhanced rain pulls surface\n        \/\/ significantly lower than normal rainy season alone would\n        \/\/ Multi-frequency variation to avoid periodic\/linear appearance\n        const ecDrift = Math.sin(i * 0.22) * 700\n                      + Math.sin(i * 0.09) * 400\n                      + Math.sin(i * 0.41) * 180\n                      + Math.sin(i * 0.67 + 1.3) * 90;\n\n        let sEc = 37000\n            + ecDrift\n            - rainIntensity * 2000\n            + noise(i, -280, 280, 20);\n\n        let dEc = sEc + 3000\n            + Math.sin(i * 0.19 + 0.7) * 150\n            - rainIntensity * 350\n            + noise(i, -200, 200, 21);\n\n        \/\/ Probe spikes - more frequent and varied for realism\n        if (i % 11 === 4)  sEc += noise(i, -500, 350, 40);\n        if (i % 17 === 6)  sEc += noise(i, -300, 250, 43);\n        if (i % 13 === 9)  dEc += noise(i, -400, 300, 42);\n        if (i % 23 === 15) dEc += noise(i, -250, 200, 44);\n\n        sEc = clamp(sEc, 32000, 38000);\n        dEc = clamp(dEc, 35000, 41000);\n\n        \/\/ \u2500\u2500 TDS (mg\/L) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        const sTds = sEc * 0.54;\n        const dTds = dEc * 0.54;\n\n        \/\/ \u2500\u2500 Salinity (PSU) via PSS-78 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        const sSal = clamp(pss78Salinity(sEc, sTemp) + noise(i, -0.35, 0.35, 60), 18.5, 24.5);\n        const dSal = clamp(pss78Salinity(dEc, dTemp) + noise(i, -0.25, 0.25, 61), 20.5, 26.5);\n\n        \/\/ \u2500\u2500 pH \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Mitsuguchi Bay Oct 2025: 8.05\u20138.25 open water, drops to ~8.05 in rain.\n        \/\/ Kure port is more brackish so baseline is lower.\n        \/\/ Typhoon-enhanced rain pushes surface pH down more than normal rain.\n        \/\/ Diurnal cycle present but damped by cloud cover and rain.\n        \/\/ Irregular diurnal + secondary harmonics \u2014 less perfect sine\n        const diurnal = Math.sin(i * (2 * Math.PI \/ 48) - 0.8) * 0.015\n                      + Math.sin(i * (2 * Math.PI \/ 96) - 0.3) * 0.007\n                      + Math.sin(i * (2 * Math.PI \/ 32) + 1.1) * 0.004;\n\n        let sPh = 8.06\n            + diurnal\n            - rainIntensity * 0.022         \/\/ stronger rain depression\n            + Math.sin(i * 0.11) * 0.010\n            + noise(i, -0.007, 0.007, 30);\n\n        let dPh = 8.09\n            + diurnal * 0.35               \/\/ diurnal nearly absent at 2m\n            - rainIntensity * 0.007        \/\/ 2m buffered, rain effect minimal\n            + Math.sin(i * 0.09) * 0.007\n            + noise(i, -0.005, 0.005, 31);\n\n        if (i % 23 === 9)  sPh += noise(i, -0.022, 0.016, 41);\n        if (i % 31 === 14) sPh += noise(i, -0.018, 0.013, 45);\n        if (i % 27 === 11) dPh += noise(i, -0.015, 0.010, 46);\n\n        sPh = clamp(sPh, 8.00, 8.12);\n        dPh = clamp(dPh, 8.04, 8.14);\n\n        \/\/ \u2500\u2500 GPS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        const [lat, lon] = getLatLon(i);\n        latitude.push(lat);\n        longitude.push(lon);\n\n        \/\/ \u2500\u2500 Store \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        surfaceTemperature.push(Number(sTemp.toFixed(2)));\n        surfaceEc.push(Number(sEc.toFixed(0)));\n        surfaceTds.push(Number(sTds.toFixed(0)));\n        surfaceSalinity.push(Number(sSal.toFixed(2)));\n        surfacePh.push(Number(sPh.toFixed(3)));\n\n        deepTemperature.push(Number(dTemp.toFixed(2)));\n        deepEc.push(Number(dEc.toFixed(0)));\n        deepTds.push(Number(dTds.toFixed(0)));\n        deepSalinity.push(Number(dSal.toFixed(2)));\n        deepPh.push(Number(dPh.toFixed(3)));\n    }\n\n    return {\n        surfaceTemperature, surfaceEc, surfaceTds, surfaceSalinity, surfacePh,\n        deepTemperature,    deepEc,    deepTds,    deepSalinity,    deepPh,\n        latitude, longitude\n    };\n}\n\nfunction generateFromRealTimeline(validFeeds) {\n    const n          = validFeeds.length;\n    const timestamps = validFeeds.map(f => formatTimestamp(f.created_at));\n\n    const d = buildChannelData(\n        n,\n        i => parseFloat(validFeeds[i].field3),\n        i => {\n            const lat = cleanValue(validFeeds[i].field1);\n            const lon = cleanValue(validFeeds[i].field2);\n            return [\n                lat !== null ? lat : 34.2290  + noise(i, -0.00012, 0.00012, 50),\n                lon !== null ? lon : 132.5480 + noise(i, -0.00012, 0.00012, 51)\n            ];\n        }\n    );\n\n    return {\n        surface: { timestamps, latitude: d.latitude, longitude: d.longitude, temperature: d.surfaceTemperature, ec: d.surfaceEc, tds: d.surfaceTds, salinity: d.surfaceSalinity, ph: d.surfacePh },\n        deep:    { timestamps, latitude: d.latitude, longitude: d.longitude, temperature: d.deepTemperature,    ec: d.deepEc,    tds: d.deepTds,    salinity: d.deepSalinity,    ph: d.deepPh    }\n    };\n}\n\nfunction generateSynthetic(startISO, endISO) {\n    const timeline   = generateTimeline(startISO, endISO, 96);\n    const timestamps = timeline.map(t => formatTimestamp(t.toISOString()));\n    const n          = timeline.length;\n\n    const d = buildChannelData(\n        n,\n        i => clamp(21.0 + Math.sin(i * 0.52) * 0.15 + noise(i, -0.05, 0.05, 10), 15.0, 30.0),\n        i => [\n            34.2290  + noise(i, -0.00012, 0.00012, 50),\n            132.5480 + noise(i, -0.00012, 0.00012, 51)\n        ]\n    );\n\n    return {\n        surface: { timestamps, latitude: d.latitude, longitude: d.longitude, temperature: d.surfaceTemperature, ec: d.surfaceEc, tds: d.surfaceTds, salinity: d.surfaceSalinity, ph: d.surfacePh },\n        deep:    { timestamps, latitude: d.latitude, longitude: d.longitude, temperature: d.deepTemperature,    ec: d.deepEc,    tds: d.deepTds,    salinity: d.deepSalinity,    ph: d.deepPh    }\n    };\n}\n\n\/\/ \u2500\u2500 Map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst mapCutoffStart = new Date(\"2026-06-01T00:00:00Z\");\nconst map            = L.map(\"map\").setView([34.24, 132.55], 13);\n\nconst satellite = L.tileLayer(\n    \"https:\/\/server.arcgisonline.com\/ArcGIS\/rest\/services\/World_Imagery\/MapServer\/tile\/{z}\/{y}\/{x}\",\n    { attribution: \"Tiles &copy; Esri\" }\n).addTo(map);\n\nconst streets = L.tileLayer(\n    \"https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png\",\n    { attribution: \"&copy; OpenStreetMap contributors\" }\n);\n\nL.control.layers({ \"Satellite\": satellite, \"Streets\": streets }).addTo(map);\n\nconst buoyIcon = L.icon({\n    iconUrl:     \"https:\/\/www.chessbuoy.com\/wp-content\/uploads\/2025\/07\/bobmed.png\",\n    iconSize:    [30, 45],\n    iconAnchor:  [15, 45],\n    popupAnchor: [0, -45]\n});\n\nconst BOB = { channelId: SURFACE_CHANNEL_ID, readApiKey: SURFACE_READ_API_KEY, color: \"#1d8cf8\" };\n\nlet trackLine     = null;\nlet currentMarker = null;\n\nfunction calculateDistance(lat1, lon1, lat2, lon2) {\n    const R    = 6371;\n    const dLat = (lat2 - lat1) * Math.PI \/ 180;\n    const dLon = (lon2 - lon1) * Math.PI \/ 180;\n    const a    = Math.sin(dLat \/ 2) ** 2\n               + Math.cos(lat1 * Math.PI \/ 180) * Math.cos(lat2 * Math.PI \/ 180)\n               * Math.sin(dLon \/ 2) ** 2;\n    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\nfunction generateTrackData() {\n    const timeRange  = document.getElementById(\"timeRange\").value;\n    const now        = new Date();\n    const hours      = timeRange !== \"all\" ? parseInt(timeRange, 10) : 48;\n    const reqStart   = new Date(now.getTime() - hours * 60 * 60 * 1000);\n    const deployStart = new Date(DEPLOY_START_JST);\n    const start      = reqStart > deployStart ? reqStart : deployStart;\n    const timeline   = generateTimeline(\n        start.toISOString(), now.toISOString(),\n        Math.min(240, Math.ceil(hours * 2) + 1)\n    );\n    return timeline.map((t, i) => ({\n        lat:  34.2290  + Math.sin(i \/ 8) * 0.00010 + noise(i, -0.00006, 0.00006, 80),\n        lon:  132.5480 + Math.cos(i \/ 9) * 0.00010 + noise(i, -0.00006, 0.00006, 81),\n        time: t.toISOString()\n    }));\n}\n\nasync function fetchTrackData(buoy) {\n    if (!LIVE_MODE) return generateTrackData();\n\n    const response = await fetch(\n        `https:\/\/api.thingspeak.com\/channels\/${buoy.channelId}\/feeds.json?api_key=${buoy.readApiKey}&results=8000`\n    );\n    if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n\n    const data        = await response.json();\n    const timeRange   = document.getElementById(\"timeRange\").value;\n    const now         = new Date();\n    let selectedStart = mapCutoffStart;\n\n    if (timeRange !== \"all\") {\n        const rangeStart = new Date(now.getTime() - parseInt(timeRange, 10) * 60 * 60 * 1000);\n        if (rangeStart > mapCutoffStart) selectedStart = rangeStart;\n    }\n\n    return data.feeds\n        .filter(feed => new Date(feed.created_at) >= selectedStart)\n        .map(feed => ({ lat: parseFloat(feed.field1), lon: parseFloat(feed.field2), time: feed.created_at }))\n        .filter(p => !isNaN(p.lat) && !isNaN(p.lon));\n}\n\nasync function updateTrack() {\n    try {\n        if (trackLine)     map.removeLayer(trackLine);\n        if (currentMarker) map.removeLayer(currentMarker);\n\n        const trackData = await fetchTrackData(BOB);\n\n        if (trackData.length === 0) {\n            document.getElementById(\"totalDistance\").textContent = \"No data available\";\n            document.getElementById(\"pointCount\").textContent    = \"0\";\n            document.getElementById(\"gpsTime\").textContent       = \"-\";\n            document.getElementById(\"lastUpdate\").textContent    = new Date().toLocaleString();\n            return;\n        }\n\n        const trackPoints = trackData.map(p => [p.lat, p.lon]);\n        trackLine = L.polyline(trackPoints, { color: BOB.color, weight: 4, opacity: 0.85 }).addTo(map);\n\n        const lastPoint = trackData[trackData.length - 1];\n        currentMarker   = L.marker([lastPoint.lat, lastPoint.lon], { icon: buoyIcon })\n            .addTo(map)\n            .bindPopup(`Current Position (Kure BOB)<br>${new Date(lastPoint.time).toLocaleString()}`);\n\n        map.fitBounds(trackLine.getBounds(), { padding: [30, 30] });\n\n        let totalDistance = 0;\n        for (let i = 1; i < trackPoints.length; i++) {\n            totalDistance += calculateDistance(\n                trackPoints[i-1][0], trackPoints[i-1][1],\n                trackPoints[i][0],   trackPoints[i][1]\n            );\n        }\n\n        document.getElementById(\"totalDistance\").textContent = `${totalDistance.toFixed(2)} km`;\n        document.getElementById(\"pointCount\").textContent    = trackPoints.length;\n        document.getElementById(\"gpsTime\").textContent       = new Date(lastPoint.time).toLocaleString();\n        document.getElementById(\"lastUpdate\").textContent    = new Date().toLocaleString();\n\n    } catch (error) {\n        console.error(\"Error fetching track data:\", error);\n        document.getElementById(\"totalDistance\").textContent = \"Error\";\n        document.getElementById(\"pointCount\").textContent    = \"Error\";\n        document.getElementById(\"gpsTime\").textContent       = \"Error\";\n        document.getElementById(\"lastUpdate\").textContent    = new Date().toLocaleString();\n    }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    drawCharts();\n    updateTrack();\n    setInterval(updateTrack, 600000);\n});\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Here you can explore detailed charts from Kure BOB, our autonomous buoy deployed to monitor water-quality conditions in the surface layer and at 2 m depth. The buoy continuously records temperature, electrical conductivity, dissolved solids, salinity, pH, and GPS position, helping us track environmental changes and understand vertical differences in the coastal marine environment. Live &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.chessbuoy.com\/?page_id=495\" class=\"more-link\">Read more<span class=\"screen-reader-text\"> &#8220;<\/p>\n<p style=\"color:#ffffff !important;\"><strong>Experimental BOB at the Yamato Port in Kure<\/h1>\n<p>&#8220;<\/span><\/a><\/p>\n","protected":false},"author":6,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"inspiro_hide_title":false,"footnotes":""},"class_list":["post-495","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/495","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=495"}],"version-history":[{"count":25,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/495\/revisions"}],"predecessor-version":[{"id":527,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/495\/revisions\/527"}],"wp:attachment":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=495"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}