Weather Station Widget HTML

 <div id="awn-weather-widget">

  <div class="awn-header">
    <div>
      <div id="awn-station-name" class="awn-station-name">
        Local Weather
      </div>
      <div id="awn-station-location" class="awn-location"></div>
    </div>

    <button
      id="awn-refresh-button"
      class="awn-refresh"
      type="button"
      aria-label="Refresh weather data"
      title="Refresh weather data"
    >
      ↻
    </button>
  </div>

  <div id="awn-loading" class="awn-message">
    Loading weather data…
  </div>

  <div
    id="awn-error"
    class="awn-message awn-error"
    hidden>
  </div>

  <div id="awn-weather-content" hidden>

    <div class="awn-temperature-panel">

      <div
        id="awn-weather-symbol"
        class="awn-weather-symbol"
        title="Current station conditions"
      ></div>

      <div class="awn-temp-block">

        <div class="awn-primary-temperature">
          <span id="awn-temperature">--</span><span class="awn-degree">°F</span>
        </div>

        <div class="awn-feels-like">
          Feels like
          <span id="awn-feels-like">--</span>°F
        </div>

      </div>

    </div>

    <div class="awn-grid">

      <!-- RAIN -->

      <div class="awn-card">

        <div class="awn-icon">
          Rain
        </div>

        <div class="awn-label">
          Rain Today
        </div>

        <div class="awn-value">
          <span id="awn-rain">--</span>
          <span class="awn-unit">in</span>
        </div>

        <div class="awn-secondary">
          Hourly:
          <span id="awn-hourly-rain">--</span> in
        </div>

      </div>


      <!-- PRESSURE -->

      <div class="awn-card">

        <div class="awn-icon">
          Pressure
        </div>

        <div class="awn-label">
          Pressure
        </div>

        <div class="awn-value">
          <span id="awn-pressure">--</span>
          <span class="awn-unit">inHg</span>
        </div>

        <div class="awn-secondary">
          <span id="awn-pressure-trend">
            Relative pressure
          </span>
        </div>

      </div>


      <!-- WIND -->

      <div class="awn-card">

        <div class="awn-icon">
          Wind
        </div>

        <div class="awn-label">
          Wind
        </div>

        <div class="awn-value">
          <span id="awn-wind-speed">--</span>
          <span class="awn-unit">mph</span>
        </div>

        <div class="awn-secondary">
          <span id="awn-wind-direction">--</span>
          ·
          <span id="awn-wind-degrees">--</span>°
        </div>

      </div>


      <!-- HUMIDITY -->

      <div class="awn-card">

        <div class="awn-icon">
          Humidity
        </div>

        <div class="awn-label">
          Humidity
        </div>

        <div class="awn-value">
          <span id="awn-humidity">--</span>
          <span class="awn-unit">%</span>
        </div>

        <div class="awn-secondary">
          Gust:
          <span id="awn-wind-gust">--</span> mph
        </div>

      </div>

    </div>


    <div class="awn-footer">
      Updated:
      <span id="awn-updated-time">--</span>
    </div>

  </div>

</div>


<style>

  #awn-weather-widget {
    box-sizing: border-box;
    width: 100%;
    max-width: 620px;
    margin: 0 auto;
    padding: 14px;
    border: 1px solid rgba(0, 0, 0, 0.12);
    border-radius: 14px;
    background: linear-gradient(145deg, #f5fbff, #e9f4fc);
    color: #203040;
    font-family: Arial, Helvetica, sans-serif;
    box-shadow: 0 4px 14px rgba(0, 40, 70, 0.12);
  }

  #awn-weather-widget *,
  #awn-weather-widget *::before,
  #awn-weather-widget *::after {
    box-sizing: border-box;
  }

  .awn-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 9px;
  }

  .awn-station-name {
    font-size: 14px;
    font-weight: 700;
  }

  .awn-location {
    margin-top: 2px;
    color: #607080;
    font-size: 9px;
  }

  .awn-refresh {
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.7);
    color: #264b65;
    font-size: 16px;
    line-height: 28px;
    cursor: pointer;
  }

  .awn-refresh:hover {
    background: #ffffff;
  }

  .awn-refresh:disabled {
    cursor: wait;
    opacity: 0.6;
  }

  .awn-message {
    padding: 12px 6px;
    text-align: center;
    color: #536575;
    font-size: 10px;
  }

  .awn-error {
    color: #a12626;
  }


  /* TOP TEMPERATURE */

  .awn-temperature-panel {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    gap: 7px;
    width: 100%;
    margin: 8px auto 14px;
    text-align: center;
    white-space: nowrap;
  }

  .awn-temp-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-width: 0;
    text-align: center;
  }

  .awn-weather-symbol {
    min-width: 30px;
    font-size: 23px;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    flex-shrink: 0;
  }

  .awn-primary-temperature {
    font-size: 26px;
    font-weight: 700;
    line-height: 1;
    white-space: nowrap;
    text-align: center;
  }

  .awn-degree {
    font-size: 12px;
    vertical-align: top;
  }

  .awn-feels-like {
    margin-top: 4px;
    color: #607080;
    font-size: 8px;
    white-space: nowrap;
    text-align: center;
    width: 100%;
  }


  /* WEATHER GRID */

  .awn-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 7px;
  }

  .awn-card {
    min-width: 0;
    padding: 10px 8px;
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: 9px;
    background: rgba(255, 255, 255, 0.72);
    text-align: center;
    overflow: hidden;
  }

  .awn-icon {
    margin-bottom: 3px;
    font-size: 9px;
    color: #617382;
    font-weight: 700;
    text-transform: uppercase;
  }

  .awn-label {
    color: #617382;
    font-size: 8px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .awn-value {
    margin-top: 4px;
    font-size: 15px;
    font-weight: 700;
    white-space: nowrap;
  }

  .awn-unit {
    font-size: 8px;
    font-weight: 400;
  }

  .awn-secondary {
    min-height: 11px;
    margin-top: 4px;
    color: #6b7c89;
    font-size: 8px;
    white-space: nowrap;
  }

  .awn-footer {
    margin-top: 10px;
    color: #6b7c89;
    font-size: 8px;
    text-align: center;
    white-space: nowrap;
  }


  /* MOBILE */

  @media (max-width: 420px) {

    #awn-weather-widget {
      padding: 11px;
    }

    .awn-temperature-panel {
      gap: 5px;
      flex-wrap: nowrap;
      justify-content: center;
    }

    .awn-primary-temperature {
      font-size: 23px;
    }

    .awn-weather-symbol {
      min-width: 27px;
      font-size: 20px;
    }

    .awn-degree {
      font-size: 11px;
    }

    .awn-feels-like {
      font-size: 7px;
    }

    .awn-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .awn-card {
      padding: 8px 5px;
    }

    .awn-value {
      font-size: 13px;
    }

    .awn-secondary {
      font-size: 7px;
    }

  }


  @media (max-width: 280px) {

    .awn-grid {
      grid-template-columns: 1fr;
    }

  }

</style>


<script>

(function () {

  "use strict";


  /*
   * GOOGLE APPS SCRIPT PROXY
   */

  const PROXY_URL =
    "https://script.google.com/macros/s/AKfycbxAwHorXlDJJ8u2akJaTfV4aE8muF8F6xBQgVGyGwrHcVSRmYweCj-zf1WKBXwSTf83/exec";


  /*
   * Refresh every five minutes.
   */

  const REFRESH_INTERVAL = 300000;


  /*
   * Weather thresholds.
   */

  const WIND_THRESHOLD_MPH = 5;

  const HUMIDITY_THRESHOLD = 95;

  const PRESSURE_DROP_THRESHOLD = 0.01;


  /*
   * Day/night settings.
   *
   * Day: 7:00 AM - 6:59 PM
   * Night: 7:00 PM - 6:59 AM
   */

  const DAY_START_HOUR = 7;

  const NIGHT_START_HOUR = 19;


  /*
   * MOBILE-SAFE WEATHER SYMBOLS
   *
   * Unicode escape sequences are used instead
   * of literal emoji characters.
   *
   * This prevents Blogger from converting
   * the characters into visible HTML entities.
   */

  const ICON_RAIN_DROP =
    "\uD83D\uDCA7";

  const ICON_RAIN =
    "\uD83C\uDF27";

  const ICON_WIND =
    "\uD83D\uDCA8";

  const ICON_CLOUD =
    "\u2601";

  const ICON_SUN =
    "\u2600";

  const ICON_SHOOTING_STAR =
    "\uD83C\uDF20";


  let activeRequest = null;


  const elements = {

    loading:
      document.getElementById("awn-loading"),

    error:
      document.getElementById("awn-error"),

    content:
      document.getElementById("awn-weather-content"),

    refreshButton:
      document.getElementById("awn-refresh-button"),

    stationName:
      document.getElementById("awn-station-name"),

    stationLocation:
      document.getElementById("awn-station-location"),

    weatherSymbol:
      document.getElementById("awn-weather-symbol"),

    temperature:
      document.getElementById("awn-temperature"),

    feelsLike:
      document.getElementById("awn-feels-like"),

    rain:
      document.getElementById("awn-rain"),

    hourlyRain:
      document.getElementById("awn-hourly-rain"),

    pressure:
      document.getElementById("awn-pressure"),

    pressureTrend:
      document.getElementById("awn-pressure-trend"),

    windSpeed:
      document.getElementById("awn-wind-speed"),

    windGust:
      document.getElementById("awn-wind-gust"),

    windDirection:
      document.getElementById("awn-wind-direction"),

    windDegrees:
      document.getElementById("awn-wind-degrees"),

    humidity:
      document.getElementById("awn-humidity"),

    updatedTime:
      document.getElementById("awn-updated-time")

  };


  /*
   * LOAD WEATHER
   */

  function loadWeather() {

    if (
      activeRequest &&
      typeof activeRequest.cleanup === "function"
    ) {

      activeRequest.cleanup();

    }


    setLoading(true);


    const callbackName =
      "awnWeatherCallback_" +
      Date.now() +
      "_" +
      Math.floor(Math.random() * 100000);


    const script =
      document.createElement("script");


    let requestFinished = false;


    const timeout =
      window.setTimeout(

        function () {

          if (requestFinished) {
            return;
          }

          requestFinished = true;

          cleanup();

          showError(
            "Weather request timed out. Please try again."
          );

          setLoading(false);

        },

        15000

      );


    function cleanup() {

      window.clearTimeout(timeout);


      if (script.parentNode) {

        script.parentNode.removeChild(script);

      }


      try {

        delete window[callbackName];

      } catch (error) {

        window[callbackName] = undefined;

      }


      if (
        activeRequest &&
        activeRequest.callbackName === callbackName
      ) {

        activeRequest = null;

      }

    }


    activeRequest = {

      callbackName: callbackName,

      cleanup: cleanup

    };


    window[callbackName] =
      function (result) {

        if (requestFinished) {
          return;
        }


        requestFinished = true;

        cleanup();


        try {

          if (
            !result ||
            !result.success
          ) {

            throw new Error(

              result && result.error

                ? result.error

                : "The weather service returned an error."

            );

          }


          displayWeather(result);


        } catch (error) {


          console.error(
            "AWN weather widget:",
            error
          );


          showError(

            "Weather data is temporarily unavailable. " +
            error.message

          );


        } finally {


          setLoading(false);

        }

      };


    script.onerror =
      function () {

        if (requestFinished) {
          return;
        }


        requestFinished = true;

        cleanup();


        showError(

          "Weather data is temporarily unavailable. " +
          "The proxy request could not be loaded."

        );


        setLoading(false);

      };


    const separator =
      PROXY_URL.indexOf("?") === -1
        ? "?"
        : "&";


    script.src =
      PROXY_URL +
      separator +
      "callback=" +
      encodeURIComponent(callbackName) +
      "&time=" +
      Date.now();


    script.async = true;


    document.body.appendChild(script);

  }


  /*
   * DISPLAY WEATHER
   */

  function displayWeather(result) {

    const station =
      result.station || {};


    const weather =
      result.weather || {};


    elements.stationName.textContent =
      station.name || "Local Weather";


    elements.stationLocation.textContent =
      station.location || "";


    elements.temperature.textContent =
      formatNumber(
        weather.temperatureF,
        1
      );


    elements.feelsLike.textContent =
      formatNumber(
        weather.feelsLikeF,
        1
      );


    elements.rain.textContent =
      formatNumber(
        weather.dailyRainIn,
        2
      );


    elements.hourlyRain.textContent =
      formatNumber(
        weather.hourlyRainIn,
        2
      );


    elements.pressure.textContent =
      formatNumber(
        weather.relativePressureInHg,
        2
      );


    elements.windSpeed.textContent =
      formatNumber(
        weather.windSpeedMph,
        1
      );


    elements.windGust.textContent =
      formatNumber(
        weather.windGustMph,
        1
      );


    elements.humidity.textContent =
      formatNumber(
        weather.humidity,
        0
      );


    const degrees =
      normalizeDegrees(
        weather.windDirectionDegrees
      );


    elements.windDegrees.textContent =
      degrees === null
        ? "--"
        : Math.round(degrees);


    elements.windDirection.textContent =
      degrees === null
        ? "--"
        : degreesToCompass(degrees);


    elements.updatedTime.textContent =
      formatObservationTime(
        weather.observationTime
      );


    const pressureTrend =
      determinePressureTrend(
        weather.relativePressureInHg
      );


    updatePressureTrendText(
      pressureTrend
    );


    updateWeatherSymbol(
      weather,
      pressureTrend
    );


    storeCurrentPressure(
      weather.relativePressureInHg
    );


    elements.error.hidden = true;

    elements.content.hidden = false;

  }


  /*
   * DYNAMIC WEATHER ICON
   */

  function updateWeatherSymbol(
    weather,
    pressureTrend
  ) {

    const icons = [];


    const rain =
      Number(weather.hourlyRainIn);


    const wind =
      Number(weather.windSpeedMph);


    const humidity =
      Number(weather.humidity);


    /*
     * Rain detected during
     * the past hour.
     */

    if (
      Number.isFinite(rain) &&
      rain > 0
    ) {

      icons.push(
        ICON_RAIN_DROP
      );

    }


    /*
     * Humidity above 95%.
     */

    if (
      Number.isFinite(humidity) &&
      humidity > HUMIDITY_THRESHOLD
    ) {

      icons.push(
        ICON_RAIN
      );

    }


    /*
     * Average wind above 5 MPH.
     */

    if (
      Number.isFinite(wind) &&
      wind > WIND_THRESHOLD_MPH
    ) {

      icons.push(
        ICON_WIND
      );

    }


    /*
     * Falling pressure.
     */

    if (
      pressureTrend === "falling"
    ) {

      icons.push(
        ICON_CLOUD
      );

    }


    /*
     * If none of the special weather
     * conditions apply, show the
     * daytime or nighttime symbol.
     */

    if (
      icons.length === 0
    ) {

      if (isDaytime()) {

        icons.push(
          ICON_SUN
        );

      } else {

        icons.push(
          ICON_SHOOTING_STAR
        );

      }

    }


    /*
     * Remove duplicate symbols.
     */

    const uniqueIcons =
      icons.filter(

        function (
          icon,
          index,
          array
        ) {

          return (
            array.indexOf(icon) ===
            index
          );

        }

      );


    elements.weatherSymbol.textContent =
      uniqueIcons.join(" ");

  }


  /*
   * PRESSURE TREND
   */

  function determinePressureTrend(
    currentPressure
  ) {

    const current =
      Number(currentPressure);


    if (
      !Number.isFinite(current)
    ) {

      return "unknown";

    }


    let previous = null;


    try {

      previous =
        Number(

          localStorage.getItem(
            "awnPreviousPressure"
          )

        );

    } catch (error) {

      return "unknown";

    }


    if (
      !Number.isFinite(previous) ||
      previous === 0
    ) {

      return "unknown";

    }


    const difference =
      current - previous;


    if (
      difference <=
      -PRESSURE_DROP_THRESHOLD
    ) {

      return "falling";

    }


    if (
      difference >=
      PRESSURE_DROP_THRESHOLD
    ) {

      return "rising";

    }


    return "steady";

  }


  /*
   * STORE PRESSURE
   */

  function storeCurrentPressure(
    pressure
  ) {

    const number =
      Number(pressure);


    if (
      Number.isFinite(number)
    ) {

      try {

        localStorage.setItem(
          "awnPreviousPressure",
          String(number)
        );


        localStorage.setItem(
          "awnPreviousPressureTime",
          String(Date.now())
        );


      } catch (error) {

        /*
         * Continue if browser storage
         * is unavailable.
         */

      }

    }

  }


  /*
   * PRESSURE TREND DISPLAY
   *
   * IMPORTANT:
   *
   * Unicode escape sequences are used here
   * instead of literal arrow characters.
   *
   * This prevents Blogger from displaying
   * things such as:
   *
   * &#8594;
   *
   * instead of the actual arrow.
   */

  function updatePressureTrendText(
    trend
  ) {

    if (
      trend === "falling"
    ) {

      elements.pressureTrend.textContent =
        "\u2193 Falling";

      return;

    }


    if (
      trend === "rising"
    ) {

      elements.pressureTrend.textContent =
        "\u2191 Rising";

      return;

    }


    if (
      trend === "steady"
    ) {

      elements.pressureTrend.textContent =
        "\u2192 Steady";

      return;

    }


    elements.pressureTrend.textContent =
      "Relative pressure";

  }


  /*
   * DAY / NIGHT
   */

  function isDaytime() {

    const hour =
      new Date().getHours();


    return (

      hour >= DAY_START_HOUR &&

      hour < NIGHT_START_HOUR

    );

  }


  /*
   * LOADING DISPLAY
   */

  function setLoading(
    isLoading
  ) {

    elements.refreshButton.disabled =
      isLoading;


    if (
      isLoading &&
      elements.content.hidden
    ) {

      elements.loading.hidden =
        false;

    } else {

      elements.loading.hidden =
        true;

    }

  }


  /*
   * ERROR DISPLAY
   */

  function showError(
    message
  ) {

    elements.loading.hidden =
      true;


    elements.error.textContent =
      message;


    elements.error.hidden =
      false;


    if (
      elements.updatedTime.textContent ===
      "--"
    ) {

      elements.content.hidden =
        true;

    }

  }


  /*
   * NUMBER FORMATTER
   */

  function formatNumber(
    value,
    decimalPlaces
  ) {

    if (
      value === null ||
      value === undefined ||
      value === ""
    ) {

      return "--";

    }


    const number =
      Number(value);


    if (
      !Number.isFinite(number)
    ) {

      return "--";

    }


    return number.toFixed(
      decimalPlaces
    );

  }


  /*
   * NORMALIZE WIND DIRECTION
   */

  function normalizeDegrees(
    value
  ) {

    if (
      value === null ||
      value === undefined ||
      value === ""
    ) {

      return null;

    }


    const degrees =
      Number(value);


    if (
      !Number.isFinite(degrees)
    ) {

      return null;

    }


    return (

      (
        degrees % 360
      ) +

      360

    ) % 360;

  }


  /*
   * WIND DIRECTION
   */

  function degreesToCompass(
    degrees
  ) {

    const directions = [

      "N",
      "NNE",
      "NE",
      "ENE",
      "E",
      "ESE",
      "SE",
      "SSE",
      "S",
      "SSW",
      "SW",
      "WSW",
      "W",
      "WNW",
      "NW",
      "NNW"

    ];


    const index =

      Math.round(
        degrees / 22.5
      ) %

      directions.length;


    return directions[index];

  }


  /*
   * OBSERVATION TIME
   */

  function formatObservationTime(
    value
  ) {

    if (!value) {

      return "Time unavailable";

    }


    let date;


    if (

      typeof value === "number" ||

      /^\d+$/.test(
        String(value)
      )

    ) {

      let timestamp =
        Number(value);


      if (
        timestamp <
        100000000000
      ) {

        timestamp *= 1000;

      }


      date =
        new Date(timestamp);


    } else {


      date =
        new Date(value);

    }


    if (
      Number.isNaN(
        date.getTime()
      )
    ) {

      return String(value);

    }


    return date.toLocaleString(

      [],

      {

        month: "short",

        day: "numeric",

        hour: "numeric",

        minute: "2-digit"

      }

    );

  }


  /*
   * MANUAL REFRESH BUTTON
   */

  elements.refreshButton
    .addEventListener(

      "click",

      loadWeather

    );


  /*
   * INITIAL LOAD
   */

  loadWeather();


  /*
   * AUTOMATIC REFRESH
   */

  window.setInterval(

    loadWeather,

    REFRESH_INTERVAL

  );


})();

</script>

No comments:

Post a Comment

Popular Items