今日の地震
設定難易度

過去24時間に日本周辺で発生した地震を、ひと目で確認できるウィジェットです。
地震の発生回数や最大震度、最大マグニチュードに加えて、どこで地震が発生したのかを日本地図上に表示します。
最新の地震だけでなく、過去24時間でもっとも規模の大きかった地震も確認できます。

ウィジェットの表示内容

気象庁の地震情報フィードから情報を取得して地図に表示します。

サイズごとの表示

  • 小:取得した直近24時間の地震件数・最大震度・最大マグニチュード、最近の地震と地図を表示します。
  • 中:情報と地図を左右に分けて読みやすく表示します。
  • 大:最近の地震などをより広い地図と一覧で確認できます。

設定手順

上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、今日の地震などの名前で保存してください。

    保存したらこのページに戻り、次の手順へ進みます。

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: red; icon-glyph: globe-asia;
    // Version: 1.0.0
    
    // =====================================================
    // 今日の地震 - Scriptable Widget
    // =====================================================
    //
    // 気象庁「防災情報XML」から
    // 過去24時間の「震源・震度に関する情報」を取得。
    //
    // 小:地震回数 / 地図 / 最大震度 / 最大M / 最新 / 最大
    // 中:左に情報 / 右に地図
    // 大:サマリー / 最近の地震 / 最大の地震 / 地図
    //
    // APIキー不要
    // Parameter不要
    //
    // データ:気象庁 防災情報XML
    // =====================================================
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const CONFIG = {
    
      FEED_URL:
        "https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml",
    
      MAP_URL:
        "https://geboku.com/up/jpn.png",
    
      MAP_CACHE:
        "jpn-map-v1.png",
    
      HOURS: 24,
    
      MAX_FETCH: 50,
    
      LARGE_LIST_COUNT: 2,
    
      REFRESH_MINUTES: 30,
    
      BG_TOP:
        new Color("#15171c"),
    
      BG_BOTTOM:
        new Color("#08090c"),
    
      TEXT:
        Color.white(),
    
      SUBTEXT:
        new Color("#a9adb7"),
    
      DIM:
        new Color("#727783"),
    
      LINE:
        new Color("#292c33")
    };
    
    
    // =====================================================
    // メイン
    // =====================================================
    
    const family =
      config.widgetFamily || "medium";
    
    let earthquakes = [];
    
    
    try {
    
      earthquakes =
        await loadEarthquakes();
    
    } catch (error) {
    
      console.log(
        "地震データ取得エラー:",
        error
      );
    
    }
    
    
    let widget;
    
    
    if (family === "small") {
    
      widget =
        await createSmallWidget(
          earthquakes
        );
    
    } else if (family === "large") {
    
      widget =
        await createLargeWidget(
          earthquakes
        );
    
    } else {
    
      widget =
        await createMediumWidget(
          earthquakes
        );
    
    }
    
    
    widget.refreshAfterDate =
      new Date(
        Date.now() +
        CONFIG.REFRESH_MINUTES *
        60 *
        1000
      );
    
    
    Script.setWidget(widget);
    
    
    if (!config.runsInWidget) {
    
      if (family === "small") {
    
        await widget.presentSmall();
    
      } else if (family === "large") {
    
        await widget.presentLarge();
    
      } else {
    
        await widget.presentMedium();
    
      }
    
    }
    
    
    Script.complete();
    
    
    // =====================================================
    // 地震情報取得
    // =====================================================
    
    async function loadEarthquakes() {
    
      const feedRequest =
        new Request(
          CONFIG.FEED_URL
        );
    
      feedRequest.timeoutInterval = 15;
    
    
      const feedXML =
        await feedRequest.loadString();
    
    
      const entries =
        feedXML.match(
          /<entry>[\s\S]*?<\/entry>/g
        ) || [];
    
    
      const targets = [];
    
    
      for (const entry of entries) {
    
        const title =
          decodeXML(
            getTag(
              entry,
              "title"
            )
          );
    
    
        if (
          title !==
          "震源・震度に関する情報"
        ) {
          continue;
        }
    
    
        const linkMatch =
          entry.match(
            /<link[^>]+href="([^"]+)"/
          );
    
    
        if (!linkMatch) {
          continue;
        }
    
    
        const updatedText =
          getTag(
            entry,
            "updated"
          );
    
    
        targets.push({
    
          url:
            linkMatch[1],
    
          updated:
            updatedText
              ? new Date(updatedText)
              : null
    
        });
    
      }
    
    
      targets.sort(
        (a, b) => {
    
          const aa =
            a.updated
              ? a.updated.getTime()
              : 0;
    
          const bb =
            b.updated
              ? b.updated.getTime()
              : 0;
    
          return bb - aa;
    
        }
      );
    
    
      const limited =
        targets.slice(
          0,
          CONFIG.MAX_FETCH
        );
    
    
      const results = [];
    
    
      for (const target of limited) {
    
        try {
    
          const request =
            new Request(
              target.url
            );
    
          request.timeoutInterval = 10;
    
    
          const xml =
            await request.loadString();
    
    
          const quake =
            parseEarthquakeXML(
              xml
            );
    
    
          if (quake) {
    
            results.push(
              quake
            );
    
          }
    
        } catch (error) {
    
          console.log(
            "地震情報取得失敗:",
            target.url
          );
    
        }
    
      }
    
    
      // ===================================================
      // 同一地震の続報を重複排除
      // ===================================================
    
      const quakeMap =
        new Map();
    
    
      for (const quake of results) {
    
        const key =
          quake.eventId ||
          [
            quake.time?.getTime(),
            quake.place,
            quake.magnitude
          ].join("_");
    
    
        if (!quakeMap.has(key)) {
    
          quakeMap.set(
            key,
            quake
          );
    
          continue;
    
        }
    
    
        const old =
          quakeMap.get(key);
    
    
        if (
          intensityValue(
            quake.maxIntensity
          ) >
          intensityValue(
            old.maxIntensity
          )
        ) {
    
          quakeMap.set(
            key,
            quake
          );
    
        }
    
      }
    
    
      let unique =
        Array.from(
          quakeMap.values()
        );
    
    
      // ===================================================
      // 過去24時間
      // ===================================================
    
      const border =
        Date.now() -
        CONFIG.HOURS *
        60 *
        60 *
        1000;
    
    
      unique =
        unique.filter(
          quake => {
    
            if (!quake.time) {
              return false;
            }
    
            return (
              quake.time.getTime() >=
              border
            );
    
          }
        );
    
    
      unique.sort(
        (a, b) =>
          b.time.getTime() -
          a.time.getTime()
      );
    
    
      return unique;
    
    }
    
    
    // =====================================================
    // XML解析
    // =====================================================
    
    function parseEarthquakeXML(xml) {
    
      const eventId =
        decodeXML(
          getTag(
            xml,
            "EventID"
          )
        );
    
    
      const originTimeText =
        getTag(
          xml,
          "OriginTime"
        );
    
    
      if (!originTimeText) {
        return null;
      }
    
    
      const time =
        new Date(
          originTimeText
        );
    
    
      // ---------------------------------------------
      // 震源
      // ---------------------------------------------
    
      let place =
        "震源不明";
    
    
      const hypocenter =
        getBlock(
          xml,
          "Hypocenter"
        );
    
    
      if (hypocenter) {
    
        const area =
          getBlock(
            hypocenter,
            "Area"
          );
    
    
        const name =
          decodeXML(
            getTag(
              area,
              "Name"
            )
          );
    
    
        if (name) {
    
          place = name;
    
        }
    
      }
    
    
      // ---------------------------------------------
      // マグニチュード
      // ---------------------------------------------
    
      let magnitude = null;
    
    
      const magMatch =
        xml.match(
          /<jmx_eb:Magnitude[^>]*>([^<]+)<\/jmx_eb:Magnitude>/
        );
    
    
      if (magMatch) {
    
        const value =
          parseFloat(
            magMatch[1]
          );
    
    
        if (!isNaN(value)) {
    
          magnitude = value;
    
        }
    
      }
    
    
      // ---------------------------------------------
      // 最大震度
      // ---------------------------------------------
    
      let maxIntensity =
        decodeXML(
          getTag(
            xml,
            "MaxInt"
          )
        );
    
    
      if (!maxIntensity) {
    
        maxIntensity = "-";
    
      }
    
    
      // ---------------------------------------------
      // 緯度 / 経度 / 深さ
      // ---------------------------------------------
    
      let latitude = null;
      let longitude = null;
      let depth = null;
    
    
      const coordinateMatch =
        hypocenter
          ? hypocenter.match(
              /<jmx_eb:Coordinate[^>]*>([^<]+)<\/jmx_eb:Coordinate>/
            )
          : null;
    
    
      if (coordinateMatch) {
    
        const coordinate =
          coordinateMatch[1];
    
    
        const match =
          coordinate.match(
            /([+-]\d+(?:\.\d+)?)([+-]\d+(?:\.\d+)?)([+-]\d+)/
          );
    
    
        if (match) {
    
          latitude =
            parseFloat(
              match[1]
            );
    
    
          longitude =
            parseFloat(
              match[2]
            );
    
    
          const depthMeter =
            Math.abs(
              parseInt(
                match[3]
              )
            );
    
    
          depth =
            Math.round(
              depthMeter / 1000
            );
    
        }
    
      }
    
    
      return {
    
        eventId,
        time,
        place,
        magnitude,
        maxIntensity,
        latitude,
        longitude,
        depth
    
      };
    
    }
    
    
    // =====================================================
    // SMALL
    // =====================================================
    
    async function createSmallWidget(data) {
    
      const widget =
        baseWidget();
    
    
      widget.setPadding(
        12,
        12,
        12,
        12
      );
    
    
      const maxIntensity =
        getMaxIntensity(
          data
        );
    
    
      const maxMag =
        getMaxMagnitude(
          data
        );
    
    
      const maxQuake =
        getMaxMagnitudeQuake(
          data
        );
    
    
      // ===================================================
      // 上下の余白を均等化
      // ===================================================
    
      widget.addSpacer();
    
    
      const content =
        widget.addStack();
    
      content.layoutVertically();
    
    
      // ---------------------------------------------
      // タイトル
      // ---------------------------------------------
    
      const title =
        content.addText(
          "今日の地震"
        );
    
      title.font =
        Font.boldSystemFont(13);
    
      title.textColor =
        CONFIG.TEXT;
    
    
      content.addSpacer(7);
    
    
      // ---------------------------------------------
      // 回数 + 地図
      // ---------------------------------------------
    
      const topRow =
        content.addStack();
    
      topRow.layoutHorizontally();
    
      topRow.centerAlignContent();
    
    
      const countBox =
        topRow.addStack();
    
      countBox.layoutVertically();
    
    
      const countRow =
        countBox.addStack();
    
      countRow.centerAlignContent();
    
    
      const count =
        countRow.addText(
          String(
            data.length
          )
        );
    
      count.font =
        Font.boldSystemFont(34);
    
      count.textColor =
        CONFIG.TEXT;
    
    
      countRow.addSpacer(2);
    
    
      const countUnit =
        countRow.addText(
          "回"
        );
    
      countUnit.font =
        Font.systemFont(9);
    
      countUnit.textColor =
        CONFIG.SUBTEXT;
    
    
      const period =
        countBox.addText(
          "過去24時間"
        );
    
      period.font =
        Font.systemFont(8);
    
      period.textColor =
        CONFIG.SUBTEXT;
    
    
      topRow.addSpacer();
    
    
      // ---------------------------------------------
      // 小サイズ地図
      // ---------------------------------------------
    
      try {
    
        const baseMap =
          await loadMapImage();
    
    
        const quakeMap =
          drawEarthquakeMap(
            baseMap,
            data
          );
    
    
        const map =
          topRow.addImage(
            quakeMap
          );
    
    
        map.imageSize =
          new Size(
            74,
            49
          );
    
        map.resizable = true;
    
      } catch (error) {
    
        console.log(
          "小地図表示エラー:",
          error
        );
    
      }
    
    
      content.addSpacer(9);
    
    
      // ---------------------------------------------
      // 最大震度 / 最大M
      // ---------------------------------------------
    
      const summary =
        content.addStack();
    
      summary.layoutHorizontally();
    
      summary.centerAlignContent();
    
    
      const intensityBox =
        summary.addStack();
    
      intensityBox.centerAlignContent();
    
    
      const intensityLabel =
        intensityBox.addText(
          "最大震度"
        );
    
      intensityLabel.font =
        Font.systemFont(8);
    
      intensityLabel.textColor =
        CONFIG.SUBTEXT;
    
    
      intensityBox.addSpacer(5);
    
    
      const intensity =
        intensityBox.addText(
          maxIntensity
        );
    
      intensity.font =
        Font.boldSystemFont(14);
    
      intensity.textColor =
        intensityColor(
          maxIntensity
        );
    
    
      summary.addSpacer();
    
    
      const magBox =
        summary.addStack();
    
      magBox.centerAlignContent();
    
    
      const magLabel =
        magBox.addText(
          "最大M"
        );
    
      magLabel.font =
        Font.systemFont(8);
    
      magLabel.textColor =
        CONFIG.SUBTEXT;
    
    
      magBox.addSpacer(5);
    
    
      const mag =
        magBox.addText(
          maxMag !== null
            ? maxMag.toFixed(1)
            : "-"
        );
    
      mag.font =
        Font.boldSystemFont(14);
    
      mag.textColor =
        magnitudeColor(
          maxMag
        );
    
    
      content.addSpacer(5);
    
    
      // ---------------------------------------------
      // 最新
      // ---------------------------------------------
    
      if (data.length) {
    
        addSmallQuakeLine(
          content,
          "最新",
          data[0],
          false
        );
    
      } else {
    
        const none =
          content.addText(
            "地震情報なし"
          );
    
        none.font =
          Font.systemFont(8);
    
        none.textColor =
          CONFIG.SUBTEXT;
    
      }
    
    
      // ---------------------------------------------
      // 最大
      // ---------------------------------------------
    
      if (maxQuake) {
    
        content.addSpacer(2);
    
    
        addSmallQuakeLine(
          content,
          "最大",
          maxQuake,
          true
        );
    
      }
    
    
      widget.addSpacer();
    
    
      return widget;
    
    }
    
    
    // =====================================================
    // SMALL 地震1行
    // =====================================================
    
    function addSmallQuakeLine(
      parent,
      label,
      quake,
      highlight
    ) {
    
      const row =
        parent.addStack();
    
      row.centerAlignContent();
    
    
      const labelText =
        row.addText(
          label
        );
    
      labelText.font =
        Font.systemFont(7);
    
      labelText.textColor =
        CONFIG.DIM;
    
    
      row.addSpacer(4);
    
    
      const time =
        row.addText(
          formatTime(
            quake.time
          )
        );
    
      time.font =
        Font.systemFont(8);
    
      time.textColor =
        highlight
          ? magnitudeColor(
              quake.magnitude
            )
          : CONFIG.SUBTEXT;
    
    
      row.addSpacer(4);
    
    
      const place =
        row.addText(
          quake.place
        );
    
      place.font =
        Font.systemFont(8);
    
      place.textColor =
        highlight
          ? magnitudeColor(
              quake.magnitude
            )
          : CONFIG.SUBTEXT;
    
      place.lineLimit = 1;
    
    }
    
    
    // =====================================================
    // MEDIUM
    // =====================================================
    
    async function createMediumWidget(data) {
    
      const widget =
        baseWidget();
    
    
      widget.setPadding(
        13,
        15,
        13,
        15
      );
    
    
      const maxIntensity =
        getMaxIntensity(
          data
        );
    
    
      const maxMag =
        getMaxMagnitude(
          data
        );
    
    
      const maxQuake =
        getMaxMagnitudeQuake(
          data
        );
    
    
      // ---------------------------------------------
      // ヘッダー
      // ---------------------------------------------
    
      const head =
        widget.addStack();
    
      head.centerAlignContent();
    
    
      const title =
        head.addText(
          "今日の地震"
        );
    
      title.font =
        Font.boldSystemFont(15);
    
      title.textColor =
        CONFIG.TEXT;
    
    
      head.addSpacer();
    
    
      const period =
        head.addText(
          "過去24時間"
        );
    
      period.font =
        Font.systemFont(8);
    
      period.textColor =
        CONFIG.DIM;
    
    
      // ===================================================
      // ヘッダー下の残り領域を上下中央
      // ===================================================
    
      widget.addSpacer();
    
    
      const body =
        widget.addStack();
    
      body.layoutHorizontally();
    
      body.centerAlignContent();
    
    
      // ===================================================
      // 左半分
      // ===================================================
    
      const left =
        body.addStack();
    
      left.layoutVertically();
    
      left.size =
        new Size(
          145,
          0
        );
    
    
      left.addSpacer();
    
    
      // ---------------------------------------------
      // 地震 / 最大震度 / 最大M
      // 横一列
      // ---------------------------------------------
    
      const summary =
        left.addStack();
    
      summary.layoutHorizontally();
    
    
      addMediumSummary(
        summary,
        "地震",
        String(
          data.length
        ),
        "回",
        CONFIG.TEXT
      );
    
    
      summary.addSpacer();
    
    
      addMediumSummary(
        summary,
        "最大震度",
        maxIntensity,
        "",
        intensityColor(
          maxIntensity
        )
      );
    
    
      summary.addSpacer();
    
    
      addMediumSummary(
        summary,
        "最大M",
        maxMag !== null
          ? maxMag.toFixed(1)
          : "-",
        "",
        magnitudeColor(
          maxMag
        )
      );
    
    
      left.addSpacer(12);
    
    
      // ---------------------------------------------
      // 最新
      // ---------------------------------------------
    
      if (data.length) {
    
        addMediumQuakeBlock(
          left,
          "最新",
          data[0]
        );
    
      }
    
    
      // ---------------------------------------------
      // 最大
      // ---------------------------------------------
    
      if (maxQuake) {
    
        left.addSpacer(8);
    
    
        addMediumQuakeBlock(
          left,
          "最大",
          maxQuake
        );
    
      }
    
    
      left.addSpacer();
    
    
      body.addSpacer(10);
    
    
      // ===================================================
      // 右半分 地図
      // ===================================================
    
      const right =
        body.addStack();
    
      right.layoutVertically();
    
      right.size =
        new Size(
          145,
          0
        );
    
    
      right.addSpacer();
    
    
      try {
    
        const baseMap =
          await loadMapImage();
    
    
        const quakeMap =
          drawEarthquakeMap(
            baseMap,
            data
          );
    
    
        const mapRow =
          right.addStack();
    
        mapRow.layoutHorizontally();
    
    
        mapRow.addSpacer();
    
    
        const map =
          mapRow.addImage(
            quakeMap
          );
    
    
        map.imageSize =
          new Size(
            145,
            97
          );
    
        map.resizable = true;
    
    
        mapRow.addSpacer();
    
      } catch (error) {
    
        console.log(
          "中地図表示エラー:",
          error
        );
    
      }
    
    
      right.addSpacer();
    
    
      widget.addSpacer();
    
    
      return widget;
    
    }
    
    
    // =====================================================
    // MEDIUM サマリー
    // =====================================================
    
    function addMediumSummary(
      parent,
      label,
      value,
      unit,
      color
    ) {
    
      const box =
        parent.addStack();
    
      box.layoutVertically();
    
    
      const labelText =
        box.addText(
          label
        );
    
      labelText.font =
        Font.systemFont(7);
    
      labelText.textColor =
        CONFIG.DIM;
    
    
      box.addSpacer(2);
    
    
      const row =
        box.addStack();
    
      row.centerAlignContent();
    
    
      const valueText =
        row.addText(
          value
        );
    
      valueText.font =
        Font.boldSystemFont(16);
    
      valueText.textColor =
        color;
    
    
      if (unit) {
    
        row.addSpacer(2);
    
    
        const unitText =
          row.addText(
            unit
          );
    
        unitText.font =
          Font.systemFont(7);
    
        unitText.textColor =
          CONFIG.SUBTEXT;
    
      }
    
    }
    
    
    // =====================================================
    // MEDIUM 地震情報
    // =====================================================
    
    function addMediumQuakeBlock(
      parent,
      label,
      quake
    ) {
    
      const labelText =
        parent.addText(
          label
        );
    
      labelText.font =
        Font.systemFont(7);
    
      labelText.textColor =
        CONFIG.DIM;
    
    
      parent.addSpacer(2);
    
    
      const place =
        parent.addText(
          quake.place
        );
    
      place.font =
        Font.semiboldSystemFont(9);
    
      place.textColor =
        CONFIG.TEXT;
    
      place.lineLimit = 1;
    
    
      parent.addSpacer(2);
    
    
      const detail =
        parent.addStack();
    
      detail.centerAlignContent();
    
    
      const time =
        detail.addText(
          formatTime(
            quake.time
          )
        );
    
      time.font =
        Font.systemFont(7);
    
      time.textColor =
        CONFIG.SUBTEXT;
    
    
      detail.addSpacer(5);
    
    
      const mag =
        detail.addText(
          quake.magnitude !== null
            ? `M${quake.magnitude.toFixed(1)}`
            : "M-"
        );
    
      mag.font =
        Font.systemFont(7);
    
      mag.textColor =
        magnitudeColor(
          quake.magnitude
        );
    
    
      detail.addSpacer(5);
    
    
      const intensity =
        detail.addText(
          `震度${quake.maxIntensity}`
        );
    
      intensity.font =
        Font.boldSystemFont(7);
    
      intensity.textColor =
        intensityColor(
          quake.maxIntensity
        );
    
    }
    
    
    // =====================================================
    // LARGE
    // =====================================================
    
    async function createLargeWidget(data) {
    
      const widget =
        baseWidget();
    
    
      widget.setPadding(
        16,
        18,
        10,
        18
      );
    
    
      const maxIntensity =
        getMaxIntensity(
          data
        );
    
    
      const maxMag =
        getMaxMagnitude(
          data
        );
    
    
      const maxQuake =
        getMaxMagnitudeQuake(
          data
        );
    
    
      // ---------------------------------------------
      // ヘッダー
      // ---------------------------------------------
    
      const head =
        widget.addStack();
    
      head.centerAlignContent();
    
    
      const title =
        head.addText(
          "今日の地震"
        );
    
      title.font =
        Font.boldSystemFont(16);
    
      title.textColor =
        CONFIG.TEXT;
    
    
      head.addSpacer();
    
    
      const period =
        head.addText(
          "過去24時間"
        );
    
      period.font =
        Font.systemFont(9);
    
      period.textColor =
        CONFIG.DIM;
    
    
      widget.addSpacer(9);
    
    
      // ---------------------------------------------
      // サマリー
      // ---------------------------------------------
    
      const summary =
        widget.addStack();
    
      summary.layoutHorizontally();
    
    
      addLargeSummary(
        summary,
        "地震",
        String(
          data.length
        ),
        "回",
        CONFIG.TEXT
      );
    
    
      summary.addSpacer();
    
    
      addLargeSummary(
        summary,
        "最大震度",
        maxIntensity,
        "",
        intensityColor(
          maxIntensity
        )
      );
    
    
      summary.addSpacer();
    
    
      addLargeSummary(
        summary,
        "最大M",
        maxMag !== null
          ? maxMag.toFixed(1)
          : "-",
        "",
        magnitudeColor(
          maxMag
        )
      );
    
    
      widget.addSpacer(10);
    
    
      // ---------------------------------------------
      // 最近の地震
      // ---------------------------------------------
    
      const recentTitle =
        widget.addText(
          "最近の地震"
        );
    
      recentTitle.font =
        Font.semiboldSystemFont(9);
    
      recentTitle.textColor =
        CONFIG.SUBTEXT;
    
    
      widget.addSpacer(4);
    
    
      if (data.length) {
    
        const recent =
          data.slice(
            0,
            CONFIG.LARGE_LIST_COUNT
          );
    
    
        for (
          let i = 0;
          i < recent.length;
          i++
        ) {
    
          addLargeQuakeRow(
            widget,
            recent[i]
          );
    
    
          if (
            i <
            recent.length - 1
          ) {
    
            widget.addSpacer(3);
    
          }
    
        }
    
      } else {
    
        const none =
          widget.addText(
            "地震情報なし"
          );
    
        none.font =
          Font.systemFont(9);
    
        none.textColor =
          CONFIG.SUBTEXT;
    
      }
    
    
      // ---------------------------------------------
      // 最大の地震
      // ---------------------------------------------
    
      if (maxQuake) {
    
        widget.addSpacer(7);
    
    
        const maxTitle =
          widget.addText(
            "最大の地震"
          );
    
        maxTitle.font =
          Font.semiboldSystemFont(9);
    
        maxTitle.textColor =
          CONFIG.SUBTEXT;
    
    
        widget.addSpacer(4);
    
    
        addLargeQuakeRow(
          widget,
          maxQuake
        );
    
      }
    
    
      widget.addSpacer(7);
    
    
      // ---------------------------------------------
      // 地図
      // ---------------------------------------------
    
      try {
    
        const baseMap =
          await loadMapImage();
    
    
        const quakeMap =
          drawEarthquakeMap(
            baseMap,
            data
          );
    
    
        const mapStack =
          widget.addStack();
    
        mapStack.layoutHorizontally();
    
    
        mapStack.addSpacer();
    
    
        const image =
          mapStack.addImage(
            quakeMap
          );
    
    
        image.imageSize =
          new Size(
            300,
            160
          );
    
        image.resizable = true;
    
    
        mapStack.addSpacer();
    
      } catch (error) {
    
        console.log(
          "大地図表示エラー:",
          error
        );
    
      }
    
    
      widget.addSpacer();
    
    
      // ---------------------------------------------
      // フッター
      // ---------------------------------------------
    
      const footer =
        widget.addStack();
    
      footer.centerAlignContent();
    
    
      const source =
        footer.addText(
          "出典:気象庁"
        );
    
      source.font =
        Font.systemFont(7);
    
      source.textColor =
        CONFIG.DIM;
    
    
      footer.addSpacer();
    
    
      const updated =
        footer.addText(
          `${formatTime(new Date())} 更新`
        );
    
      updated.font =
        Font.systemFont(7);
    
      updated.textColor =
        CONFIG.DIM;
    
    
      return widget;
    
    }
    
    
    // =====================================================
    // LARGE サマリー
    // =====================================================
    
    function addLargeSummary(
      parent,
      label,
      value,
      unit,
      color
    ) {
    
      const box =
        parent.addStack();
    
      box.layoutVertically();
    
    
      const labelText =
        box.addText(
          label
        );
    
      labelText.font =
        Font.systemFont(9);
    
      labelText.textColor =
        CONFIG.DIM;
    
    
      box.addSpacer(3);
    
    
      const row =
        box.addStack();
    
      row.centerAlignContent();
    
    
      const valueText =
        row.addText(
          value
        );
    
      valueText.font =
        Font.boldSystemFont(22);
    
      valueText.textColor =
        color;
    
    
      if (unit) {
    
        row.addSpacer(2);
    
    
        const unitText =
          row.addText(
            unit
          );
    
        unitText.font =
          Font.systemFont(8);
    
        unitText.textColor =
          CONFIG.SUBTEXT;
    
      }
    
    }
    
    
    // =====================================================
    // LARGE 地震1行
    // =====================================================
    
    function addLargeQuakeRow(
      parent,
      quake
    ) {
    
      const row =
        parent.addStack();
    
      row.centerAlignContent();
    
    
      const time =
        row.addText(
          formatTime(
            quake.time
          )
        );
    
      time.font =
        Font.systemFont(9);
    
      time.textColor =
        CONFIG.DIM;
    
    
      row.addSpacer(8);
    
    
      const place =
        row.addText(
          quake.place
        );
    
      place.font =
        Font.mediumSystemFont(9);
    
      place.textColor =
        CONFIG.TEXT;
    
      place.lineLimit = 1;
    
    
      row.addSpacer();
    
    
      const mag =
        row.addText(
          quake.magnitude !== null
            ? `M${quake.magnitude.toFixed(1)}`
            : "M-"
        );
    
      mag.font =
        Font.systemFont(9);
    
      mag.textColor =
        magnitudeColor(
          quake.magnitude
        );
    
    
      row.addSpacer(8);
    
    
      const intensity =
        row.addText(
          `震度${quake.maxIntensity}`
        );
    
      intensity.font =
        Font.boldSystemFont(9);
    
      intensity.textColor =
        intensityColor(
          quake.maxIntensity
        );
    
    }
    
    
    // =====================================================
    // 地図画像取得
    // =====================================================
    
    async function loadMapImage() {
    
      const fm =
        FileManager.local();
    
    
      const dir =
        fm.joinPath(
          fm.documentsDirectory(),
          "EarthquakeWidget"
        );
    
    
      if (!fm.fileExists(dir)) {
    
        fm.createDirectory(
          dir,
          true
        );
    
      }
    
    
      const path =
        fm.joinPath(
          dir,
          CONFIG.MAP_CACHE
        );
    
    
      if (
        fm.fileExists(path)
      ) {
    
        return fm.readImage(
          path
        );
    
      }
    
    
      const request =
        new Request(
          CONFIG.MAP_URL
        );
    
    
      request.timeoutInterval = 15;
    
    
      const image =
        await request.loadImage();
    
    
      fm.writeImage(
        path,
        image
      );
    
    
      return image;
    
    }
    
    
    // =====================================================
    // 地図+震源描画
    // =====================================================
    
    function drawEarthquakeMap(
      baseImage,
      earthquakes
    ) {
    
      const W = 600;
      const H = 400;
    
    
      const ctx =
        new DrawContext();
    
    
      ctx.size =
        new Size(
          W,
          H
        );
    
    
      ctx.opaque = false;
    
      ctx.respectScreenScale = true;
    
    
      ctx.drawImageInRect(
        baseImage,
        new Rect(
          0,
          0,
          W,
          H
        )
      );
    
    
      // 古い地震から描画
      for (
        let i =
          earthquakes.length - 1;
        i >= 0;
        i--
      ) {
    
        const quake =
          earthquakes[i];
    
    
        if (
          quake.latitude === null ||
          quake.longitude === null
        ) {
          continue;
        }
    
    
        const p =
          quakeToMapPoint(
            quake.latitude,
            quake.longitude,
            W,
            H
          );
    
    
        if (!p) {
          continue;
        }
    
    
        let radius = 7;
    
    
        if (
          quake.magnitude !== null
        ) {
    
          radius =
            Math.max(
              7,
              Math.min(
                19,
                quake.magnitude * 2.1
              )
            );
    
        }
    
    
        const color =
          intensityColor(
            quake.maxIntensity
          );
    
    
        // グロー
        ctx.setFillColor(
          new Color(
            color.hex,
            0.20
          )
        );
    
    
        ctx.fillEllipse(
          new Rect(
            p.x - radius * 1.8,
            p.y - radius * 1.8,
            radius * 3.6,
            radius * 3.6
          )
        );
    
    
        // 本体
        ctx.setFillColor(
          color
        );
    
    
        ctx.fillEllipse(
          new Rect(
            p.x - radius,
            p.y - radius,
            radius * 2,
            radius * 2
          )
        );
    
    
        // 最新地震を白枠
        if (i === 0) {
    
          ctx.setStrokeColor(
            Color.white()
          );
    
    
          ctx.setLineWidth(3);
    
    
          ctx.strokeEllipse(
            new Rect(
              p.x - radius - 5,
              p.y - radius - 5,
              radius * 2 + 10,
              radius * 2 + 10
            )
          );
    
        }
    
      }
    
    
      return ctx.getImage();
    
    }
    
    
    // =====================================================
    // 緯度経度 → jpn.png 座標
    // =====================================================
    
    function quakeToMapPoint(
      lat,
      lon,
      width,
      height
    ) {
    
      if (
        lat < 23 ||
        lat > 47 ||
        lon < 122 ||
        lon > 151
      ) {
    
        return null;
    
      }
    
    
      const latN =
        (lat - 24) /
        (46 - 24);
    
    
      const lonN =
        (lon - 123) /
        (147 - 123);
    
    
      let x =
        width *
        (
          0.11 +
          lonN * 0.72
        );
    
    
      x +=
        width *
        (
          latN - 0.5
        ) *
        0.09;
    
    
      let y =
        height *
        (
          0.88 -
          latN * 0.76
        );
    
    
      y -=
        height *
        (
          lonN - 0.5
        ) *
        0.09;
    
    
      return {
        x,
        y
      };
    
    }
    
    
    // =====================================================
    // Widgetベース
    // =====================================================
    
    function baseWidget() {
    
      const widget =
        new ListWidget();
    
    
      const gradient =
        new LinearGradient();
    
    
      gradient.locations =
        [0, 1];
    
    
      gradient.colors =
        [
          CONFIG.BG_TOP,
          CONFIG.BG_BOTTOM
        ];
    
    
      widget.backgroundGradient =
        gradient;
    
    
      return widget;
    
    }
    
    
    // =====================================================
    // 最大震度
    // =====================================================
    
    function getMaxIntensity(data) {
    
      if (!data.length) {
        return "-";
      }
    
    
      let max = "-";
    
      let maxValue = -1;
    
    
      for (const quake of data) {
    
        const value =
          intensityValue(
            quake.maxIntensity
          );
    
    
        if (
          value > maxValue
        ) {
    
          maxValue = value;
    
          max =
            quake.maxIntensity;
    
        }
    
      }
    
    
      return max;
    
    }
    
    
    // =====================================================
    // 最大マグニチュード
    // =====================================================
    
    function getMaxMagnitude(data) {
    
      const quake =
        getMaxMagnitudeQuake(
          data
        );
    
    
      return quake
        ? quake.magnitude
        : null;
    
    }
    
    
    // =====================================================
    // 最大Mの地震
    // =====================================================
    
    function getMaxMagnitudeQuake(data) {
    
      let result = null;
    
    
      for (const quake of data) {
    
        if (
          quake.magnitude === null
        ) {
          continue;
        }
    
    
        if (
          !result ||
          quake.magnitude >
          result.magnitude
        ) {
    
          result = quake;
    
        }
    
      }
    
    
      return result;
    
    }
    
    
    // =====================================================
    // 震度 → 数値
    // =====================================================
    
    function intensityValue(value) {
    
      const table = {
    
        "1": 1,
        "2": 2,
        "3": 3,
        "4": 4,
    
        "5-": 5,
        "5弱": 5,
    
        "5+": 6,
        "5強": 6,
    
        "6-": 7,
        "6弱": 7,
    
        "6+": 8,
        "6強": 8,
    
        "7": 9
    
      };
    
    
      return (
        table[value] ??
        0
      );
    
    }
    
    
    // =====================================================
    // 震度色
    // =====================================================
    
    function intensityColor(value) {
    
      const level =
        intensityValue(
          value
        );
    
    
      if (level >= 9) {
    
        return new Color(
          "#c13cff"
        );
    
      }
    
    
      if (level >= 7) {
    
        return new Color(
          "#ff3b30"
        );
    
      }
    
    
      if (level >= 5) {
    
        return new Color(
          "#ff9500"
        );
    
      }
    
    
      if (level >= 3) {
    
        return new Color(
          "#ffd60a"
        );
    
      }
    
    
      return new Color(
        "#65d46e"
      );
    
    }
    
    
    // =====================================================
    // マグニチュード色
    // =====================================================
    
    function magnitudeColor(value) {
    
      if (
        value === null ||
        value === undefined ||
        isNaN(value)
      ) {
    
        return CONFIG.SUBTEXT;
    
      }
    
    
      if (value >= 7) {
    
        return new Color(
          "#d94cff"
        );
    
      }
    
    
      if (value >= 6) {
    
        return new Color(
          "#ff3b30"
        );
    
      }
    
    
      if (value >= 5) {
    
        return new Color(
          "#ff9500"
        );
    
      }
    
    
      if (value >= 4) {
    
        return new Color(
          "#ffd60a"
        );
    
      }
    
    
      return new Color(
        "#65d46e"
      );
    
    }
    
    
    // =====================================================
    // 時刻
    // =====================================================
    
    function formatTime(date) {
    
      const formatter =
        new DateFormatter();
    
    
      formatter.locale =
        "ja_JP";
    
    
      formatter.dateFormat =
        "HH:mm";
    
    
      return formatter.string(
        date
      );
    
    }
    
    
    // =====================================================
    // XML Utility
    // =====================================================
    
    function getTag(
      xml,
      tag
    ) {
    
      if (!xml) {
        return "";
      }
    
    
      const escaped =
        tag.replace(
          /[-/\\^$*+?.()|[\]{}]/g,
          "\\$&"
        );
    
    
      const regex =
        new RegExp(
          `<(?:[^:>]+:)?${escaped}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[^:>]+:)?${escaped}>`
        );
    
    
      const match =
        xml.match(
          regex
        );
    
    
      return match
        ? stripTags(
            match[1]
          ).trim()
        : "";
    
    }
    
    
    function getBlock(
      xml,
      tag
    ) {
    
      if (!xml) {
        return "";
      }
    
    
      const escaped =
        tag.replace(
          /[-/\\^$*+?.()|[\]{}]/g,
          "\\$&"
        );
    
    
      const regex =
        new RegExp(
          `<(?:[^:>]+:)?${escaped}(?:\\s[^>]*)?>[\\s\\S]*?<\\/(?:[^:>]+:)?${escaped}>`
        );
    
    
      const match =
        xml.match(
          regex
        );
    
    
      return match
        ? match[0]
        : "";
    
    }
    
    
    function stripTags(text) {
    
      return String(
        text || ""
      )
      .replace(
        /<[^>]+>/g,
        ""
      );
    
    }
    
    
    function decodeXML(text) {
    
      return String(
        text || ""
      )
      .replace(
        /&amp;/g,
        "&"
      )
      .replace(
        /&lt;/g,
        "<"
      )
      .replace(
        /&gt;/g,
        ">"
      )
      .replace(
        /&quot;/g,
        "\""
      )
      .replace(
        /&apos;/g,
        "'"
      )
      .replace(
        /&#39;/g,
        "'"
      );
    
    }
  3. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」は空欄で構いません。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

更新について

地震情報は約30分ごとに更新されます。
地図画像は初回のみ取得し、その後はScriptable内に保存された画像を利用するため、毎回地図画像をダウンロードすることはありません。
※ウィジェットの実際の更新タイミングは、iOS側の判断によって前後する場合があります。

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.0.0
動作確認日 2026-09-27

表示・使用時のポイント

気象庁の地震情報フィードから情報を取得して地図に表示します。取得したデータの範囲で直近24時間の情報を集計します。

更新履歴

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
件数が0になる
取得範囲に該当する地震がない場合は0です。
地図が表示されない
背景地図の画像取得を含め、通信を確認して再実行してください。
ニュースと件数が違う
対象時間や取得できた情報の範囲が違う場合があります。気象庁の元情報を確認してください。

注意事項・制限

  • このウィジェットは、気象庁が公開している情報を見やすく表示するためのものです。
    緊急地震速報や防災通知を目的としたものではありません。
    大きな地震が発生した場合は、気象庁や自治体などの公式な防災情報を確認してください。
  • 緊急地震速報や通知を提供する機能はありません。
  • 避難などの判断には、この表示だけでなく気象庁や自治体の最新情報を確認してください。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

気象庁 防災情報XML:地震情報フィードを取得しています。

SHARE

この記事をシェア