{"id":452,"date":"2026-04-22T07:49:06","date_gmt":"2026-04-22T07:49:06","guid":{"rendered":"https:\/\/www.chessbuoy.com\/?page_id=452"},"modified":"2026-04-22T12:01:05","modified_gmt":"2026-04-22T12:01:05","slug":"big-bob-2-at-mitsuguchi-bay-aquaculture-region","status":"publish","type":"page","link":"https:\/\/www.chessbuoy.com\/?page_id=452","title":{"rendered":"<p style=\"color:#ffffff !important;\"><strong>Big BOB 2 at Mitsuguchi Bay Aquaculture Region<\/h1>"},"content":{"rendered":"\n\n<p><strong>Here you can explore detailed charts from BIG BOB, our larger-sized autonomous buoy deployed at an oyster farm in the Mitsuguchi Bay off the Yasuura coast in Hiroshima. This buoy continuously monitors water quality parameters such as temperature, salinity, pH, and dissolved solids, providing insights into the local marine environment. The data helps us track environmental changes, understand seasonal variations, and support sustainable aquaculture practices in the region.<\/strong><\/p>\n\n\n\n<style>\nhtml, body {\n    background: #08111f !important;\n    color: #eaf2ff !important;\n}\n\n.container {\n    background: rgba(255,255,255,0.04) !important;\n}\n\n.chart-container {\n    background: rgba(255,255,255,0.05) !important;\n    border: 1px solid rgba(255,255,255,0.1) !important;\n}\n\n.chart-title {\n    color: #ffffff !important;\n}\n\n.controls {\n    background: rgba(255,255,255,0.05) !important;\n}\n\n.controls input {\n    background: #0f2238 !important;\n    color: #ffffff !important;\n}\n\n.loading {\n    color: #cbd8f5 !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.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@media (max-width: 768px) {\n    body {\n        padding: 12px;\n    }\n\n    .container {\n        padding: 16px;\n    }\n\n    .controls label {\n        display: block;\n        margin: 10px 0;\n    }\n\n    .controls button {\n        margin-top: 8px;\n    }\n\n    .chart-container {\n        min-height: 400px;\n        padding: 16px 12px 24px 12px;\n    }\n\n    .chart-container canvas {\n        height: 330px !important;\n    }\n}\n<\/style>\n\n<div class=\"container\">\n    <div class=\"subtitle\">Live water-quality data from Mitsuguchi Bay<\/div>\n\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=\"chart-container\">\n        <div class=\"chart-title\">Temperature (\u00b0C)<\/div>\n        <canvas id=\"temperatureChart\"><\/canvas>\n    <\/div>\n\n    <div class=\"chart-container\">\n        <div class=\"chart-title\">pH<\/div>\n        <canvas id=\"phChart\"><\/canvas>\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\n<script>\nlet originalData = null;\nlet chartInstances = {};\n\nconst cutoffStartUTC = \"2026-04-22T07:38:00Z\";\nconst cutoffStartDateOnly = \"2026-04-22\";\n\nconst phFaultStartUTC = \"2026-04-05T03:30:00Z\";\nconst phUpperLimit = 9.0;\n\nfunction applyPhQC(value, timestamp) {\n    if (value === null || value === undefined || value === '' || value === 'NA') {\n        return null;\n    }\n\n    const p = parseFloat(value);\n    if (isNaN(p)) return null;\n\n    const t = new Date(timestamp);\n    const faultStart = new Date(phFaultStartUTC);\n\n    if (t >= faultStart && p > phUpperLimit) {\n        return null;\n    }\n\n    return p;\n}\n\nasync function fetchAllFields(startISO, endISO) {\n    const url = `https:\/\/api.thingspeak.com\/channels\/2788373\/feeds.json?start=${startISO}&end=${endISO}`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    const json = await response.json();\n    if (!json.feeds || json.feeds.length === 0) return null;\n\n    const cutoffStart = new Date(cutoffStartUTC);\n\n    const filteredFeeds = json.feeds.filter(e => {\n        const entryDate = new Date(e.created_at);\n        return entryDate >= cutoffStart;\n    });\n\n    if (filteredFeeds.length === 0) return null;\n\n    return {\n        timestamps: filteredFeeds.map(e => {\n            const d = new Date(e.created_at);\n            return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {\n                hour: '2-digit',\n                minute: '2-digit'\n            });\n        }),\n        temperature: filteredFeeds.map(e => parseFloat(e.field3)),\n        ph: filteredFeeds.map(e => applyPhQC(e.field7, e.created_at))\n    };\n}\n\nfunction filterValidData(data) {\n    return data.map(v => (v === null || v === undefined || isNaN(v) || v < -900) ? null : v);\n}\n\nfunction destroyCharts() {\n    Object.values(chartInstances).forEach(chart => {\n        if (chart) chart.destroy();\n    });\n    chartInstances = {};\n}\n\nfunction createChart(canvasId, label, data, color) {\n    const ctx = document.getElementById(canvasId).getContext('2d');\n    const validData = filterValidData(data);\n    const nonNullData = validData.filter(v => v !== null);\n\n    if (nonNullData.length === 0) {\n        return null;\n    }\n\n    const minVal = Math.min(...nonNullData);\n    const maxVal = Math.max(...nonNullData);\n    const padding = (maxVal - minVal) * 0.1 || 1;\n\n    let yScaleOptions = {\n        title: {\n            display: true,\n            text: label,\n            color: '#dfeaff',\n            font: { size: 13, weight: '600' }\n        },\n        ticks: {\n            color: '#bfd0ea'\n        },\n        grid: {\n            color: 'rgba(255,255,255,0.08)',\n            drawBorder: false\n        }\n    };\n\n    if (canvasId === \"temperatureChart\") {\n        yScaleOptions.min = Math.max(0, minVal - 1);\n        yScaleOptions.max = maxVal + 1;\n    } else if (canvasId === \"phChart\") {\n        yScaleOptions.min = 6;\n        yScaleOptions.max = 10;\n    } else {\n        yScaleOptions.min = Math.max(0, minVal - padding);\n        yScaleOptions.max = maxVal + padding;\n    }\n\n    const gradient = ctx.createLinearGradient(0, 0, 0, 360);\n    gradient.addColorStop(0, color.replace('rgb', 'rgba').replace(')', ',0.30)'));\n    gradient.addColorStop(1, color.replace('rgb', 'rgba').replace(')', ',0.00)'));\n\n    return new Chart(ctx, {\n        type: 'line',\n        data: {\n            labels: originalData.timestamps,\n            datasets: [{\n                label: label,\n                data: validData,\n                borderColor: color,\n                backgroundColor: gradient,\n                fill: true,\n                tension: 0.35,\n                borderWidth: 3,\n                pointRadius: 1.5,\n                pointHoverRadius: 5,\n                pointBackgroundColor: color,\n                pointBorderWidth: 0,\n                spanGaps: true\n            }]\n        },\n        options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: {\n                padding: {\n                    bottom: 8\n                }\n            },\n            interaction: {\n                mode: 'index',\n                intersect: false\n            },\n            plugins: {\n                legend: {\n                    labels: {\n                        color: '#e6f0ff',\n                        font: { size: 13, weight: '600' }\n                    }\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: {\n                        enabled: true,\n                        mode: 'x'\n                    },\n                    zoom: {\n                        wheel: { enabled: true },\n                        pinch: { enabled: true },\n                        mode: 'x'\n                    }\n                }\n            },\n            scales: {\n                x: {\n                    title: {\n                        display: true,\n                        text: 'Date and Time',\n                        color: '#dfeaff',\n                        font: { size: 13, weight: '600' },\n                        padding: { top: 10 }\n                    },\n                    ticks: {\n                        color: '#bfd0ea',\n                        autoSkip: true,\n                        maxTicksLimit: 5,\n                        maxRotation: 0,\n                        minRotation: 0,\n                        padding: 10\n                    },\n                    grid: {\n                        color: 'rgba(255,255,255,0.06)',\n                        drawBorder: false\n                    }\n                },\n                y: yScaleOptions\n            }\n        }\n    });\n}\n\nasync function drawCharts(start = null, end = null) {\n    try {\n        const loadingMessage = document.getElementById('loadingMessage');\n        loadingMessage.style.display = 'block';\n        loadingMessage.innerHTML = 'Loading chart data...';\n\n        const now = new Date();\n        const cutoffStart = new Date(cutoffStartUTC);\n\n        const startISO = start || cutoffStart.toISOString();\n        const endISO = end || now.toISOString();\n\n        const data = await fetchAllFields(startISO, endISO);\n\n        destroyCharts();\n\n        if (!data) {\n            loadingMessage.innerHTML = `<div style=\"color:#ff8080;\">No data available for this range<\/div>`;\n            return;\n        }\n\n        originalData = data;\n        loadingMessage.style.display = 'none';\n\n        chartInstances.temperature = createChart('temperatureChart', 'Temperature (\u00b0C)', data.temperature, 'rgb(255,99,132)');\n        chartInstances.ph = createChart('phChart', 'pH', data.ph, 'rgb(255,159,64)');\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    } catch (err) {\n        destroyCharts();\n        document.getElementById('loadingMessage').innerHTML = `<div style=\"color:#ff8080;\">Error loading charts: ${err.message}<\/div>`;\n    }\n}\n\nfunction applyDateFilter() {\n    const startInput = document.getElementById('startDate').value;\n    const endInput = document.getElementById('endDate').value;\n\n    if (!startInput || !endInput) {\n        alert('Select both dates');\n        return;\n    }\n\n    const startDate = new Date(startInput + \"T00:00:00\");\n    const endDate = new Date(endInput + \"T23:59:59\");\n\n    drawCharts(startDate.toISOString(), endDate.toISOString());\n}\n\nfunction resetZoom() {\n    Object.values(chartInstances).forEach(chart => {\n        if (chart) chart.resetZoom();\n    });\n}\n\ndocument.addEventListener('DOMContentLoaded', () => drawCharts());\n<\/script>\n\n<script>\nlet originalData = null;\nlet chartInstances = {};\n\nconst cutoffStartUTC = \"2026-04-02T07:16:00Z\";\nconst cutoffStartDateOnly = \"2026-04-02\";\n\n\/\/ Temporary pH QC settings\nconst phFaultStartUTC = \"2026-04-05T03:30:00Z\"; \/\/ adjust if needed\nconst phUpperLimit = 9.0;\n\nfunction applyPhQC(value, timestamp) {\n    if (value === null || value === undefined || value === '' || value === 'NA') {\n        return null;\n    }\n\n    const p = parseFloat(value);\n    if (isNaN(p)) return null;\n\n    const t = new Date(timestamp);\n    const faultStart = new Date(phFaultStartUTC);\n\n    \/\/ Apply QC only after the later fault period starts\n    if (t >= faultStart && p > phUpperLimit) {\n        return null;\n    }\n\n    return p;\n}\n\nasync function fetchAllFields(startISO, endISO) {\n    const url = `https:\/\/api.thingspeak.com\/channels\/2788373\/feeds.json?start=${startISO}&end=${endISO}`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    const json = await response.json();\n    if (!json.feeds || json.feeds.length === 0) return null;\n\n    const cutoffStart = new Date(cutoffStartUTC);\n\n    const filteredFeeds = json.feeds.filter(e => {\n        const entryDate = new Date(e.created_at);\n        return entryDate >= cutoffStart;\n    });\n\n    if (filteredFeeds.length === 0) return null;\n\n    return {\n        timestamps: filteredFeeds.map(e => {\n            const d = new Date(e.created_at);\n            return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {\n                hour: '2-digit',\n                minute: '2-digit'\n            });\n        }),\n        temperature: filteredFeeds.map(e => parseFloat(e.field3)),\n        ec: filteredFeeds.map(e => parseFloat(e.field4)),\n        tds: filteredFeeds.map(e => parseFloat(e.field5)),\n        salinity: filteredFeeds.map(e => parseFloat(e.field6)),\n        ph: filteredFeeds.map(e => applyPhQC(e.field7, e.created_at))\n    };\n}\n\nfunction filterValidData(data) {\n    return data.map(v => (v === null || v === undefined || isNaN(v) || v < -900) ? null : v);\n}\n\nfunction destroyCharts() {\n    Object.values(chartInstances).forEach(chart => {\n        if (chart) chart.destroy();\n    });\n    chartInstances = {};\n}\n\nfunction createChart(canvasId, label, data, color) {\n    const ctx = document.getElementById(canvasId).getContext('2d');\n    const validData = filterValidData(data);\n    const nonNullData = validData.filter(v => v !== null);\n\n    if (nonNullData.length === 0) {\n        return null;\n    }\n\n    const minVal = Math.min(...nonNullData);\n    const maxVal = Math.max(...nonNullData);\n    const padding = (maxVal - minVal) * 0.1 || 1;\n\n    let yScaleOptions = {\n        title: {\n            display: true,\n            text: label,\n            color: '#dfeaff',\n            font: { size: 13, weight: '600' }\n        },\n        ticks: {\n            color: '#bfd0ea'\n        },\n        grid: {\n            color: 'rgba(255,255,255,0.08)',\n            drawBorder: false\n        }\n    };\n\n    if (canvasId === \"temperatureChart\") {\n        yScaleOptions.min = Math.max(0, minVal - 1);\n        yScaleOptions.max = maxVal + 1;\n    } else if (canvasId === \"phChart\") {\n        yScaleOptions.min = 6;\n        yScaleOptions.max = 10;\n    } else {\n        yScaleOptions.min = Math.max(0, minVal - padding);\n        yScaleOptions.max = maxVal + padding;\n    }\n\n    const gradient = ctx.createLinearGradient(0, 0, 0, 360);\n    gradient.addColorStop(0, color.replace('rgb', 'rgba').replace(')', ',0.30)'));\n    gradient.addColorStop(1, color.replace('rgb', 'rgba').replace(')', ',0.00)'));\n\n    return new Chart(ctx, {\n        type: 'line',\n        data: {\n            labels: originalData.timestamps,\n            datasets: [{\n                label: label,\n                data: validData,\n                borderColor: color,\n                backgroundColor: gradient,\n                fill: true,\n                tension: 0.35,\n                borderWidth: 3,\n                pointRadius: 1.5,\n                pointHoverRadius: 5,\n                pointBackgroundColor: color,\n                pointBorderWidth: 0,\n                spanGaps: true\n            }]\n        },\n        options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: {\n                padding: {\n                    bottom: 8\n                }\n            },\n            interaction: {\n                mode: 'index',\n                intersect: false\n            },\n            plugins: {\n                legend: {\n                    labels: {\n                        color: '#e6f0ff',\n                        font: { size: 13, weight: '600' }\n                    }\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: {\n                        enabled: true,\n                        mode: 'x'\n                    },\n                    zoom: {\n                        wheel: { enabled: true },\n                        pinch: { enabled: true },\n                        mode: 'x'\n                    }\n                }\n            },\n            scales: {\n                x: {\n                    title: {\n                        display: true,\n                        text: 'Date and Time',\n                        color: '#dfeaff',\n                        font: { size: 13, weight: '600' },\n                        padding: { top: 10 }\n                    },\n                    ticks: {\n                        color: '#bfd0ea',\n                        autoSkip: true,\n                        maxTicksLimit: 5,\n                        maxRotation: 0,\n                        minRotation: 0,\n                        padding: 10\n                    },\n                    grid: {\n                        color: 'rgba(255,255,255,0.06)',\n                        drawBorder: false\n                    }\n                },\n                y: yScaleOptions\n            }\n        }\n    });\n}\n\nasync function drawCharts(start = null, end = null) {\n    try {\n        const loadingMessage = document.getElementById('loadingMessage');\n        loadingMessage.style.display = 'block';\n        loadingMessage.innerHTML = 'Loading chart data...';\n\n        const now = new Date();\n        const cutoffStart = new Date(cutoffStartUTC);\n\n        const startISO = start || cutoffStart.toISOString();\n        const endISO = end || now.toISOString();\n\n        const data = await fetchAllFields(startISO, endISO);\n\n        destroyCharts();\n\n        if (!data) {\n            loadingMessage.innerHTML = `<div style=\"color:#ff8080;\">No data available for this range<\/div>`;\n            return;\n        }\n\n        originalData = data;\n        loadingMessage.style.display = 'none';\n\n        chartInstances.temperature = createChart('temperatureChart', 'Temperature (\u00b0C)', data.temperature, 'rgb(255,99,132)');\n        chartInstances.ec = createChart('ecChart', 'EC (\u03bcS\/cm)', data.ec, 'rgb(0,196,255)');\n        chartInstances.tds = createChart('tdsChart', 'TDS (ppm)', data.tds, 'rgb(0,214,143)');\n        chartInstances.salinity = createChart('salinityChart', 'Salinity (PSU)', data.salinity, 'rgb(168,85,247)');\n        chartInstances.ph = createChart('phChart', 'pH', data.ph, 'rgb(255,159,64)');\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    } catch (err) {\n        destroyCharts();\n        document.getElementById('loadingMessage').innerHTML = `<div style=\"color:#ff8080;\">Error loading charts: ${err.message}<\/div>`;\n    }\n}\n\nfunction applyDateFilter() {\n    const startInput = document.getElementById('startDate').value;\n    const endInput = document.getElementById('endDate').value;\n\n    if (!startInput || !endInput) {\n        alert('Select both dates');\n        return;\n    }\n\n    const startDate = new Date(startInput + \"T00:00:00\");\n    const endDate = new Date(endInput + \"T23:59:59\");\n\n    drawCharts(startDate.toISOString(), endDate.toISOString());\n}\n\nfunction resetZoom() {\n    Object.values(chartInstances).forEach(chart => {\n        if (chart) chart.resetZoom();\n    });\n}\n\ndocument.addEventListener('DOMContentLoaded', () => drawCharts());\n<\/script>\n\n<!-- \/wp:post-content -->\n\n<!-- wp:html -->\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/leaflet\/1.9.4\/leaflet.css\" \/>\n\n<style>\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 {\n    color: #b8c8e6;\n}\n\n.info-box span {\n    color: #ffffff;\n    font-weight: 600;\n}\n\n.leaflet-popup-content {\n    color: #000000;\n}\n\n@media (max-width: 768px) {\n    #map {\n        height: 450px;\n    }\n\n    .map-controls {\n        text-align: left;\n    }\n\n    .map-controls select,\n    .map-controls button {\n        display: block;\n        width: 100%;\n        margin: 8px 0;\n    }\n}\n<\/style>\n\n<div class=\"map-wrapper\">\n    <div class=\"map-subtitle\">Buoy movement tracking in Mitsuguchi Bay<\/div>\n\n    <div id=\"map\"><\/div>\n\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\n        <button onclick=\"updateTrack()\">Update Track<\/button>\n\n        <div class=\"info-box\">\n            <div>Total Distance (BOB): <span id=\"totalDistance\">Calculating&#8230;<\/span><\/div>\n            <div>Number of Points (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\/leaflet\/1.9.4\/leaflet.js\"><\/script>\n\n<script>\nconst cutoffStart = new Date('2026-04-02T16:16:00+09:00');\n\nconst map = L.map('map').setView([34.37835, 132.69216], 13);\n\nconst satellite = L.tileLayer(\n    'https:\/\/server.arcgisonline.com\/ArcGIS\/rest\/services\/World_Imagery\/MapServer\/tile\/{z}\/{y}\/{x}',\n    {\n        attribution: 'Tiles &copy; Esri'\n    }\n).addTo(map);\n\nconst streets = L.tileLayer(\n    'https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png',\n    {\n        attribution: '&copy; OpenStreetMap contributors'\n    }\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 = {\n    channelId: '2788373',\n    readApiKey: '8O6R6C70M4R2HLSK',\n    color: '#1d8cf8'\n};\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 =\n        Math.sin(dLat \/ 2) ** 2 +\n        Math.cos(lat1 * Math.PI \/ 180) *\n        Math.cos(lat2 * Math.PI \/ 180) *\n        Math.sin(dLon \/ 2) ** 2;\n    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n    return R * c;\n}\n\nasync function fetchTrackData(buoy) {\n    const results = 8000;\n    const response = await fetch(\n        `https:\/\/api.thingspeak.com\/channels\/${buoy.channelId}\/feeds.json?api_key=${buoy.readApiKey}&results=${results}`\n    );\n\n    if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n    const timeRange = document.getElementById('timeRange').value;\n    const now = new Date();\n\n    let selectedStartDate = cutoffStart;\n\n    if (timeRange !== 'all') {\n        const hours = parseInt(timeRange, 10);\n        const rangeStart = new Date(now.getTime() - hours * 60 * 60 * 1000);\n\n        if (rangeStart > cutoffStart) {\n            selectedStartDate = rangeStart;\n        }\n    }\n\n    return data.feeds\n        .filter(feed => new Date(feed.created_at) >= selectedStartDate)\n        .map(feed => ({\n            lat: parseFloat(feed.field1),\n            lon: parseFloat(feed.field2),\n            time: feed.created_at\n        }))\n        .filter(point => !isNaN(point.lat) && !isNaN(point.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\n        \/\/ trackLine = L.polyline(trackPoints, {\n       \/\/     color: BOB.color,\n       \/\/     weight: 4,\n       \/\/     opacity: 0.85\n       \/\/  }).addTo(map);\n\n        const lastPoint = trackData[trackData.length - 1];\n\n        currentMarker = L.marker([lastPoint.lat, lastPoint.lon], { icon: buoyIcon })\n            .addTo(map)\n            .bindPopup(`Current Position (BOB)<br>${new Date(lastPoint.time).toLocaleString()}`);\n\n        map.setView([lastPoint.lat, lastPoint.lon], 13);\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    } 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\nupdateTrack();\nsetInterval(updateTrack, 600000);\n<\/script>\n<!-- \/wp:html -->\n\n<!-- wp:paragraph -->\n<p><\/p>\n<!-- \/wp:paragraph -->","protected":false},"excerpt":{"rendered":"<p>Here you can explore detailed charts from BIG BOB, our larger-sized autonomous buoy deployed at an oyster farm in the Mitsuguchi Bay off the Yasuura coast in Hiroshima. This buoy continuously monitors water quality parameters such as temperature, salinity, pH, and dissolved solids, providing insights into the local marine environment. The data helps us track &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.chessbuoy.com\/?page_id=452\" class=\"more-link\">Read more<span class=\"screen-reader-text\"> &#8220;<\/p>\n<p style=\"color:#ffffff !important;\"><strong>Big BOB 2 at Mitsuguchi Bay Aquaculture Region<\/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-452","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/452","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=452"}],"version-history":[{"count":6,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/452\/revisions"}],"predecessor-version":[{"id":473,"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=\/wp\/v2\/pages\/452\/revisions\/473"}],"wp:attachment":[{"href":"https:\/\/www.chessbuoy.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=452"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}