River Level Widget

This river monitoring widget is a dynamic, lightweight web tool designed to provide real-time water level data across England by seamlessly integrating with the UK Environment Agency API.

Upon loading, the application intelligently detects the user's IP location, instantly querying and displaying data from the closest river stations within a five-mile radius.

To ensure network efficiency and relevance, the widget features a geographic guardrail that politely pauses auto-detection for non-UK IP addresses, instead directing them to a built-in search bar that allows users to manually look up specific towns or river names.

Beyond simply displaying current depths, the application acts as a real-time analytical tool by actively calculating short-term flow trends.

By fetching and comparing the latest gauge reading against the data captured in the previous fifteen-minute window, the widget visualises water movement using directional arrows - instantly indicating whether a river is rising, falling, or remaining stable.

To maximise situational awareness and public safety, the interface incorporates an automated, tiered visual alert system driven by local historical data. The widget continuously evaluates current levels against each station's unique "typical high" threshold.

If a river hits 90% of its normal high range, the display shifts to a cautionary amber; however, if the water strictly exceeds its historical limit, or is near the high while actively surging (rising 2 centimeters or more in just fifteen minutes), the interface triggers a prominent red alert complete with warning badges. This ensures that flash flood risks are immediately obvious, transforming raw telemetry into easily digestible, actionable insights.

River Levels

Detecting location...

Buy Me A Coffee
River Levels
<div id="river-widget" style="font-family: sans-serif; border: 1px solid #ccc; padding: 15px; border-radius: 8px; background: #ffffff; max-width: 380px;">
  <h3 style="margin-top: 0; font-size: 1.2em; color: #333; border-bottom: 2px solid #00a3a6; padding-bottom: 8px;">Local River Levels</h3>
  
  <div style="display: flex; gap: 8px; margin-bottom: 15px;">
    <input type="text" id="river-search" placeholder="Search town or river..." style="flex-grow: 1; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px;">
    <button id="river-btn" style="background: #00a3a6; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: bold;">Search</button>
  </div>

  <div id="river-data" style="color: #666; font-size: 0.95em;">Detecting location...</div>
</div>

<script>
(async function() {
  const output = document.getElementById('river-data');
  const searchInput = document.getElementById('river-search');
  const searchBtn = document.getElementById('river-btn');

  async function renderStations(stationList) {
    if (!stationList || stationList.length === 0) {
      output.innerHTML = '<div style="color: #d32f2f;">No river stations found for this search.</div>';
      return;
    }

    const topStations = stationList.filter(s => s.stationReference).slice(0, 5);
    output.innerHTML = 'Loading latest data...';

    const requests = topStations.map(async (s) => {
      try {
        const wiski = s.stationReference;
        
        let displayName = s.label;
        if (s.riverName) {
          const rName = s.riverName.toLowerCase().startsWith('river') ? s.riverName : `River ${s.riverName}`;
          if (!s.label.toLowerCase().includes(s.riverName.toLowerCase())) {
            displayName = `${rName} (${s.label})`;
          }
        }

        const [stationRes, readingsRes] = await Promise.all([
          fetch(`https://environment.data.gov.uk/flood-monitoring/id/stations/${wiski}`),
          fetch(`https://environment.data.gov.uk/flood-monitoring/id/stations/${wiski}/readings?_limit=2&parameter=level&_sorted`)
        ]);

        if (!stationRes.ok || !readingsRes.ok) return '';
        
        const stationData = await stationRes.json();
        const readingsData = await readingsRes.json();
        
        const scale = stationData.items?.stageScale;
        const readings = readingsData.items;

        if (readings && readings.length > 0) {
          const latest = readings[0];
          const previous = readings.length > 1 ? readings[1] : null;

          const val = latest.value;
          const time = new Date(latest.dateTime).toLocaleTimeString('en-GB', {hour: '2-digit', minute:'2-digit'});
          
          let trendArrow = '→'; 
          let isRapidlyRising = false;
          
          if (previous) {
            const change = val - previous.value;
            if (change > 0.005) trendArrow = '▲'; 
            if (change < -0.005) trendArrow = '▼'; 
            if (change >= 0.02) isRapidlyRising = true;
          }

          let isExceedingHigh = false;
          let isNearHigh = false;
          let contextHtml = '';
          
          if (scale && scale.typicalRangeLow !== undefined && scale.typicalRangeHigh !== undefined) {
            contextHtml = `<br/><small style="color: #666;">Normal: ${scale.typicalRangeLow}m – ${scale.typicalRangeHigh}m</small>`;
            
            if (val > scale.typicalRangeHigh) isExceedingHigh = true; 
            else if (val >= (scale.typicalRangeHigh * 0.9)) isNearHigh = true; 
          }

          let levelColor = '#2c3e50'; 
          let warningBadge = '';

          if (isExceedingHigh) {
            levelColor = '#d32f2f'; 
            warningBadge = `<span style="background: #ffebee; color: #d32f2f; padding: 2px 6px; border-radius: 4px; font-size: 0.7em; font-weight: bold; margin-left: 6px; display: inline-block; vertical-align: middle;">⚠️ HIGH LEVEL</span>`;
          } else if (isNearHigh && isRapidlyRising) {
            levelColor = '#d32f2f'; 
            warningBadge = `<span style="background: #ffebee; color: #d32f2f; padding: 2px 6px; border-radius: 4px; font-size: 0.7em; font-weight: bold; margin-left: 6px; display: inline-block; vertical-align: middle;">⚠️ RAPID RISE</span>`;
          } else if (isNearHigh) {
            levelColor = '#e67e22'; 
          }

          return `<li style="margin-bottom: 12px; border-bottom: 1px solid #eee; padding-bottom: 8px;">
                    <strong style="color: #2c3e50;">${displayName}</strong><br/>
                    <span style="font-size: 1.2em; font-weight: bold; color: ${levelColor};">${val}m ${trendArrow}</span> 
                    ${warningBadge}
                    <small style="color: #888; display: block; margin-top: 2px;">(at ${time})</small>
                    ${contextHtml}
                   </li>`;
        }
        return '';
      } catch (err) {
        return ''; 
      }
    });

    const results = await Promise.all(requests);
    const validResults = results.filter(r => r !== '').join('');
    
    if (!validResults) {
      output.innerHTML = 'No recent readings available for these stations.';
    } else {
      output.innerHTML = `<ul style="list-style: none; padding: 0; margin: 0;">${validResults}</ul>`;
    }
  }

  async function searchRivers(query) {
    if (!query) return;
    output.innerHTML = `Searching for "${query}"...`;
    try {
      const res = await fetch(`https://environment.data.gov.uk/flood-monitoring/id/stations?search=${encodeURIComponent(query)}&parameter=level`);
      const data = await res.json();
      await renderStations(data.items);
    } catch (e) {
      output.innerHTML = 'Search failed. Please try again.';
    }
  }

  async function autoLocate() {
    try {
      const ipRes = await fetch('https://get.geojs.io/v1/ip/geo.json');
      if (!ipRes.ok) throw new Error('IP fetch failed');
      const ipData = await ipRes.json();
      
      // INTERNATIONAL CHECK: Stop automatic tracking if outside the UK
      if (ipData.country_code !== 'GB') {
        output.innerHTML = 'You appear to be outside the UK. Please use the search bar above to find English river stations.';
        return; // Exits the function early so we don't query the EA API
      }
      
      const lat = ipData.latitude;
      const lon = ipData.longitude;
      const city = ipData.city || 'your area';
      
      output.innerHTML = `Found location: <strong>${city}</strong>. Finding nearby rivers...`;
      
      const eaRes = await fetch(`https://environment.data.gov.uk/flood-monitoring/id/stations?lat=${lat}&long=${lon}&dist=8&parameter=level`);
      const eaData = await eaRes.json();
      
      await renderStations(eaData.items);
    } catch (e) {
      console.error(e);
      output.innerHTML = 'Could not detect location. Please use the search bar above.';
    }
  }

  searchBtn.addEventListener('click', () => searchRivers(searchInput.value));
  searchInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') searchRivers(searchInput.value);
  });

  autoLocate();
})();
</script>

Post a Comment

0 Comments

Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.

Post a Comment (0)