🌀

AI Editorとともに作る、災害時に衛星撮像すべき場所の提案システム

に公開

今回は災害対応の事例を題材に検証を行いたいと思います。
あわせて、AI Editorを使いこなしていきたいと思い、新しい機能をCursorで、Claudeさんと一緒につくってみようとおもいます。

やりたいこと

  • 地震や大雨があったときに、 対応エリアが広すぎてどこを撮像してよいかわからない
  • そんなときに、複数の要素から撮像候補を提案してくれて、理由を説明してくれるAgent

使える資源、制約

  • 日本エリアであれば国交省が多種多様なデータを管理、更新してくれています。今回は下記を利用します
    • 国土数値情報ダウンロードサイト: https://nlftp.mlit.go.jp/ksj/index.html
      • 1kmメッシュ別将来推計人口
        • 令和2年の国勢調査に基づき、2070年までの250mメッシュ別の将来人口について、男女別・年齢(5歳階級)別に推計
      • 土地利用3次メッシュデータ
        • (1kmメッシュ)毎に、各利用区分(田、その他の農用地、森林、荒地、建物用地、幹線交通用地、湖沼、河川等)の面積を整備したもの
      • 学校データ
        • 幼稚園、小学校、中学校、義務教育学校、高等学校、中等教育学校、特別支援学校、大学、高等専門学校等
      • 医療機関データ
        • 医療法に基づく「病院」「(一般)診療所」「歯科診療所」の地点、名称、所在地
  • データがないと何もできないので一旦上記データを利用して日本国内事例で検討する
    • 海外データへの応用はNextStep
  • 衛星が一回に撮像できるサイズは縦横10kmの正方形とします。
    • 大型衛星は数百キロ撮像できるものがありますが、1台しか運用していなくて同じ地域を撮像できるのに10日以上待たないといけないことになります。 災害時等は複数台運用している、小型衛星で撮像する機会のほうが多くその場合は撮像可能サイズも小さくなります(解像度は高い)。
    • 参考:https://sorabatake.jp/279/

処理フロー概要

  1. 災害イベント取得、大枠のAreaOfInterest(AOI)取得
  2. 人口データ、土地利用データ、公共施設データ取得
  3. 1と2のデータを統合して観点ごとに、撮像候補プランを作成
  4. 衛星撮像依頼

この記事では、
1は2024年石川県で発生した集中豪雨のデータを簡易化して利用します
2は事前に準備するものとします。(OnDemandで自動ダウンロードするように拡張予定)
3がメインディッシュで、4は対象外とします

データ準備

まず今回参考に利用させていただく災害情報として、発生後の被害ではなく、当時取得できただろう、災害予想情報を下にしたいと思います。 一刻も早く衛星での撮像地域を判断する必要があるためです。
下記の気象庁、キキクルの危険度分布:危険と、災害切迫(警戒レベル5相当)を想定データとして参考にしています。ただ、予想情報のGeoJson等は見つけられなかったため、かなり簡易化しているので本当の情報とは異なっています。

https://www.jma.go.jp/jma/press/2409/21a/Ishikawa_tokukei.pdf


また上記、国土数値情報ダウンロードサイトから対象地域のデータDownloadしておきます。

種類 対象ファイル ファイル名規則
1kmメッシュ別将来推計人口 1km_mesh_2024_17_GML.zip 石川県(17)
土地利用3次メッシュデータ L03-a-21_5536-jgd2011_GML.zip 石川県(5536,5537,5636,5637)
学校データ P29-23_17_GML.zip P29:学校データ, 23:2023年作成, 17:石川県
医療機関データ P04-20_17_GML.zi P04:医療機関データ, 20:2020年作成, 17:石川県

また、その後プロンプトで使う想定をしてデータフォーマットの説明をコピーしておきます。

  • 1kmメッシュ別将来推計人口
属性名
(かっこ内はshp属性名)	説明	属性の型
基準地域メッシュコード
(MESH_ID)	基準地域メッシュコード
(3次メッシュコード)	文字列型(CharacterString)
行政区域コード
(SHICODE)	市区町村を分類する5桁のコード。地方公共団体を一意に識別するためのものである。 JIS規格(JIS X 0401)+ JIS規格(JIS X 0402)に準拠する。
https://www.soumu.go.jp/toukei_toukatsu/index/seido/9-5.htm	コードリスト「行政区域コード」
2020年男女計総数人口
(PTN_2020)	2020年の総数人口	実数型(Real)
20XX年秘匿記号
(HITOKU_20XX)	秘匿対象の合算先メッシュを示す記号	文字列型(ChatacterString)
20XX年合算先メッシュ
(GASSAN_20XX)	秘匿対象の人口を加算する合算先のメッシュコード	文字列型(ChatacterString)
20XX年男女計総数人口(秘匿なし)
(PTN_20XX)	秘匿を除く、20XX年の総数人口	実数型(Real)
20XX年男女計総数人口
(PT00_20XX)	20XX年の総数人口	実数型(Real)

  • 土地利用3次メッシュデータ
属性名
(かっこ内はshp属性名)	説明	属性の型
メッシュコード
(L03b_001)	3次メッシュコード
8桁×100+左下を00とし附番したコード
(3次メッシュ1/10細分メッシュコード)	文字型(10桁)
土地利用種別
(L03b_002)	当該メッシュにおける土地利用種別。
衛星写真撮影年月日
(L03b_003)	当該メッシュ土地利用種別を判読した衛星写真の撮影年月日	西暦年4桁、月2桁、日2桁で表す数字8文字

土地利用種別(2021年度、2016年度、2014年度、2009年度)
コード	種別	定義
0100	田	湿田・乾田・沼田・蓮田及び田とする。
0200	その他の農用地	麦・陸稲・野菜・草地・芝地・りんご・梨・桃・ブドウ・茶・桐・はぜ・こうぞ・しゅろ等を栽培する土地とする。
0300	-	-
0400	-	-
0500	森林	多年生植物の密生している地域とする。
0600	荒地	しの地・荒地・がけ・岩・万年雪・湿地・採鉱地等で旧土地利用データが荒地であるところとする。
0700	建物用地	住宅地・市街地等で建物が密集しているところとする。
0800	-	-
0901	道路	道路などで、面的に捉えられるものとする。
0902	鉄道	鉄道・操車場などで、面的にとらえられるものとする。
1000	その他の用地	運動競技場、空港、競馬場・野球場・学校・港湾地区・人工造成地の空地等とする。
1100	河川地及び湖沼	人工湖・自然湖・池・養魚場等で平水時に常に水を湛えているところ及び河川・河川区域の河川敷とする。
1200	-	-
1300	-	-
1400	海浜	海岸に接する砂、れき、岩の区域とする。
1500	海水域	隠顕岩、干潟、シーパースも海に含める。
1600	ゴルフ場	ゴルフ場のゴルフコースの集まっている部分のフェアウエイ及びラフの外側と森林の境目を境界とする。

  • 学校データ
属性情報	属性名
(かっこ内はshp属性名)	説明	属性の型
位置	学校の位置	点型(GM_ Point)
行政区域コード
(P29_001)	都道府県コードと市区町村コードからなる、学校が存在する行政区を特定するためのコード	コードリスト型
「行政区域コード」
学校コード
(P29_002)	全国の学校に設定された固有の「学校コード」	文字列型
学校分類
(P29_003)	学校の種別を特定するためのコード	コードリスト型
「学校分類コード」
名称
(P29_004)	学校の正式名称	文字列型
所在地
(P29_005)	学校の市区町村名を省いた所在地	文字列型
管理者コード
(P29_006)	学校の管理者を区分するためのコード	コードリスト型
「管理者コード」
休校区分
(P29_007)	学校の休校中情報を特定するためのコード	コードリスト型
「休校コード」
キャンパスコード
(P29_008)	学校のキャンパスを区分するためのコード	コードリスト型
「キャンパスコード」
学校名備考
(P29_009)	キャンパスの名称等	文字列型

  • 医療機関データ
属性名
(かっこ内はshp属性名)	説明	属性の型
地点	医療機関の位置	点型(GM_Point)
医療機関分類
(P04_001)	医療機関の分類	コードリスト「医療機関分類コード」
施設名称
(P04_002)	医療施設の名称	文字列型(CharacterString)
所在地
(P04_003)	医療機関の所在地の住所	文字列型(CharacterString)
診療科目1
(P04_004)	当該施設が有する診療科目	文字列型(CharacterString)
診療科目2
(P04_005)	当該施設が有する診療科目(「診療科目1」が全角127文字を超える場合使用)	文字列型(CharacterString)
診療科目3
(P04_006)	当該施設が有する診療科目(「診療科目2」が全角127文字を超える場合使用)	文字列型(CharacterString)
開設者分類
(P04_007)	当該施設(病院のみ)を開設した組織または個人の分類	コードリスト「開設者分類コード」
病床数
(P04_008)	当該施設が有する病床総数	整数型(Integer)
救急告示病院
(P04_009)	医療機関が「救急病院等を定める省令」(昭和39年2月20日厚生省令第8号)に基づき告示されているかを示す値」	コードリスト「救急告示病院区分コード」
指定あり=1、指定なし=9
災害拠点病院
(P04_010)	医療機関の「災害時における医療体制の充実強化について」(平成24年3月21日医政発0321第2号厚生労働省医政局)に基づく分類	コードリスト「災害拠点病院区分コード」
基幹=1、地域=2、指定なし=9

Cursor + Caludeの設定

下記のページを参考に Cursor Pro購入と、ClaudeのAPI Keyの設定を行いました。

  • Cursor利用プランの違い理解

https://qiita.com/setowatson/items/6d7f96c0e64c9cb8a8f4

  • Cursorの基本的な使い方理解(日常の開発に使えるTips満載なので一読をおすすめです)

https://note.com/nike_cha_n/n/nd0f7566019ae

  • 2025年3月Cursorの大幅アップデートの内容解説

https://zenn.dev/kikagaku/articles/b074d9fbf01479

基本的にはニケちゃんの記事がとても実践的なのですが、 ほんの最近使い始めた私には、Chat,Composerといわれて?となりました。
調べてみると、どうやら最新のCursorでは上記2つは統合されたようなので 気にしなくてよいです。
本当に半年で様変わりしていきますね。GithubのPremiumサービスもはじまり、今年が普及期、本格導入期なのだと思います。

開発の流れ

当初はPythonで作成しようと考えていましたが、
地図上の描画等を考えるとmapbox 等と連携しやすいWebAppのほうが簡単で
公開もしやすいとおもい 今回も下記 firebase vue template からスタートしました。
https://zenn.dev/singularity/articles/firebase-startup-kit

最初の指示プロンプト

  • 一番最初に下記をいれて、地図表示がまずできるようになりました。 (mapBoxaccessTokenは事前にSignInして発行)
まず、firebase-vue3-startup-kit をコピーしたので、この構造を理解して、 現在HelloWorldが表示されているところを、disasterAOIAnalyze component に置き換えて、 mapBoxで地図を描画するだけのコンポーネントを追加して。 accessTokenを設定したら地図が描画できるようととのえて
  • 次にデータ読み込み、表示を指示します。
assets/data 以下に以下のファイルを格納しました。
地図への描画を追加してください。

| 種類 | 対象ファイル |ファイル名規則 |
| ---- | ---- | ---- |
| 1kmメッシュ別将来推計人口 | 1km_mesh_2024_17_GML.zip | 石川県(17) |
| 土地利用3次メッシュデータ | L03-a-21_5536-jgd2011_GML.zip | 石川県(5536,5537,5636,5637) |
| 学校データ |P29-23_17_GML.zip | P29:学校データ, 23:2023年作成, 17:石川県 |
| 医療機関データ|P04-20_17_GML.zi | P04:医療機関データ, 20:2020年作成, 17:石川県 |

それぞれのフォーマットは以下のようになっています
... 略

以下のように表示html, 読み込みロジックは作ってくれましたが、実際のデータの読み込みはうまくいかずエラーだらけです

DisasterAOIAnalyze.vue
template>
  <div class="disaster-aoi-analyze">
    <div ref="mapContainer" class="map-container"></div>
    <div class="layer-controls">
      <h3 class="text-lg font-bold mb-2">表示レイヤー</h3>
      <div class="control-item">
        <input type="checkbox" id="danger-layer" v-model="showDangerLayer" />
        <label for="danger-layer">浸水害危険度</label>
      </div>
      <div class="control-item">
        <input type="checkbox" id="population-layer" v-model="showPopulationLayer" />
        <label for="population-layer">人口分布</label>
      </div>

mapDataLoader.ts
export function createMeshPolygon(meshId: string): number[][] {
  const [lng, lat] = meshIdToLatLng(meshId);
  const meshSize = 1 / 80; // 3次メッシュの大きさ(約1km)
  
  return [
    [lng, lat],
    [lng + meshSize, lat],
    [lng + meshSize, lat + meshSize],
    [lng, lat + meshSize],
    [lng, lat],
  ];
}

export function createPopulationLayer(data: PopulationMeshData[]): FeatureCollection {
  return {
    type: 'FeatureCollection',
    features: data.map((mesh): Feature => ({
      type: 'Feature',
      geometry: {
        type: 'Polygon',
        coordinates: [createMeshPolygon(mesh.MESH_ID)],
      },
      properties: {
        population: mesh.PT00_2024,
        meshId: mesh.MESH_ID,
      },
    })),
  };
}

geojson の読み込み指示

  • データのパスを指定すれば読み込んで理解してくれると期待したが、うまくいかなかったのでDataFormatにあわせた指示をだすようにします。
各interface がgeoJsonのfeatureであることを理解して修正してください。 追加データはproperties 配下に定義されています。
パスは修正してfetchは成功している、fetch してもテキストのgeojson ファイルなのでそのままではjson()でよみだせないので修正してほしい。

学校、病院データはgeoJsonを素直に表示

上記の指示の結果作成されたコードで問題なく表示できた

    // 学校データの読み込み
    console.log("Loading school data...");
    const schoolResponse = await fetch("/src/assets/data/P29-23_17_GML/P29-23_17.geojson");
    const schoolText = await schoolResponse.text();
    schoolData.value = JSON.parse(schoolText);
    console.log("School data loaded:", schoolData.value.features.length, "features");

    // 医療機関データの読み込み
    console.log("Loading medical data...");
    const medicalResponse = await fetch("/src/assets/data/P04-20_17_GML/P04-20_17.geojson");
    const medicalText = await medicalResponse.text();
    medicalData.value = JSON.parse(medicalText);
    console.log("Medical data loaded:", medicalData.value.features.length, "features");

  map.addSource("schools", {
    type: "geojson",
    data: schoolData.value as FeatureCollection,
  });
  map.addLayer({
    id: SCHOOL_LAYER_ID,
    type: "circle",
    source: "schools",
    paint: {
      "circle-radius": 6,
      "circle-color": "#4a90e2",
      "circle-stroke-width": 2,
      "circle-stroke-color": "#ffffff",
    },
    layout: {
      visibility: "none",
    },
  });

  console.log("Adding medical source...");
  map.addSource("medical", {
    type: "geojson",
    data: medicalData.value as FeatureCollection,
  });
  map.addLayer({
    id: MEDICAL_LAYER_ID,
    type: "circle",
    source: "medical",
    paint: {
      "circle-radius": 6,
      "circle-color": "#e24a4a",
      "circle-stroke-width": 2,
      "circle-stroke-color": "#ffffff",
    },
    layout: {
      visibility: "none",
    },
  });

人口データxml の読み込み指示

xmlはagentまかせではなく、自分でファイルの中身を読んで、どのようにParseするかの指示を与える必要がありました。

xmlファイルのParseの仕方をかえて、
<gml:Curve gml:id="cv2257_0">
と、
<gml:Surface gml:id="sf2257">
はセットで出てくるので それをセットで保存する。
後にでてくる<ksj:bounds xlink:href="#sf0"/> で指定されたsurface の対応する Curve のcoordinateとして使ってほしい

上記の結果、かなり複雑な2020-2050までの1kmメッシュごとのxml人口データをParseできるようになりました。素晴らしい!

export const POPULATION_COLOR_THRESHOLDS = [
  { threshold: 0, color: "#ffffff", label: "0人" },
  { threshold: 100, color: "#ffffcc", label: "100人" },
  { threshold: 500, color: "#ffeda0", label: "500人" },
  { threshold: 1000, color: "#fed976", label: "1,000人" },
  { threshold: 5000, color: "#feb24c", label: "5,000人" },
  { threshold: 10000, color: "#fd8d3c", label: "10,000人" },
] as const;

export async function parsePopulationXML(xmlPath: string): Promise<PopulationMeshData[]> {
  try {
    const response = await fetch(xmlPath);
    const xmlText = await response.text();

    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xmlText, "text/xml");

    // メッシュの境界情報を取得
    const surfaceDataMap = new Map<string, SurfaceData>();

    // Curveの情報を先に取得
    const curves = xmlDoc.getElementsByTagName("gml:Curve");
    const curveCoordinates = new Map<string, number[][]>();

    for (const curve of curves) {
      const curveId = curve.getAttribute("gml:id");
      if (!curveId) {
        continue;
      }

      const posLists = curve.getElementsByTagName("gml:posList");
      if (posLists.length === 0) {
        continue;
      }

      const [posList] = posLists;
      if (!posList.textContent) {
        continue;
      }

      const coordinates = posList.textContent
        .trim()
        .split(/\s+/u)
        .map(Number)
        .reduce((acc, value, index) => {
          if (index % 2 === 0) {
            // 緯度(lat)を先に取得
            acc.push([0, value]);
          } else {
            // 経度(lng)を後で設定
            acc[acc.length - 1][0] = value;
          }
          return acc;
        }, [] as number[][]);
      curveCoordinates.set(curveId, coordinates);
    }

    // Surfaceの情報を取得し、対応するCurveと紐付け
    const surfaces = xmlDoc.getElementsByTagName("gml:Surface");
    for (const surface of surfaces) {
      const surfaceId = surface.getAttribute("gml:id");
      if (!surfaceId) {
        continue;
      }

      const curveMembers = surface.getElementsByTagName("gml:curveMember");
      if (curveMembers.length === 0) {
        continue;
      }

      const [curveMember] = curveMembers;
      const curveRef = curveMember.getAttribute("xlink:href");
      const curveId = curveRef?.replace("#_", "");

      if (!curveId || !curveCoordinates.has(curveId)) {
        continue;
      }

      surfaceDataMap.set(surfaceId, {
        surfaceId,
        curveId,
        coordinates: curveCoordinates.get(curveId) || [],
      });
    }

    // 人口データを取得
    const meshDataList: PopulationMeshData[] = [];
    const meshElements = xmlDoc.getElementsByTagName("ksj:estimatedPopulationMesh1km");

    for (const mesh of meshElements) {
      const getElementText = (tagName: string): string => {
        const elements = mesh.getElementsByTagName(`ksj:${tagName}`);
        if (elements.length === 0) {
          const elementsWithoutNS = mesh.getElementsByTagName(tagName);
          if (elementsWithoutNS.length === 0) {
            return "0";
          }
          return elementsWithoutNS[0].textContent?.trim() || "0";
        }
        return elements[0].textContent?.trim() || "0";
      };

      const meshId = getElementText("meshCode");
      const boundsElements = mesh.getElementsByTagName("ksj:bounds");
      if (boundsElements.length === 0) {
        continue;
      }

      const boundsRef = boundsElements[0].getAttribute("xlink:href");
      const surfaceId = boundsRef?.replace("#", "");

      if (!surfaceId || !surfaceDataMap.has(surfaceId)) {
        continue;
      }

      const surfaceData = surfaceDataMap.get(surfaceId);
      if (!surfaceData) {
        continue;
      }

      const data: PopulationMeshData = {
        MESH_ID: meshId,
        SHICODE: getElementText("administrativeAreaCode"),
        PTN_2020: Math.round(Number(getElementText("totalPopulationOf2020"))),
        PTN_2024: Math.round(Number(getElementText("totalPopulationIn2025"))),
        PT00_2024: Math.round(Number(getElementText("totalPopulationIn2025"))),
        coordinates: surfaceData.coordinates,
      };
      meshDataList.push(data);
    }

    return meshDataList;
  } catch (error) {
    console.error("Error parsing population XML:", error);
    return [];
  }
}
export function createPopulationLayer(data: PopulationMeshData[]): FeatureCollection {
  const features = data.map((mesh): Feature<Polygon> => {
    if (!mesh.coordinates || mesh.coordinates.length === 0) {
      return {
        type: "Feature",
        geometry: {
          type: "Polygon",
          coordinates: [[]],
        },
        properties: {
          population: mesh.PT00_2024,
          meshId: mesh.MESH_ID,
          color: getPopulationColor(mesh.PT00_2024),
        },
      };
    }

    return {
      type: "Feature",
      geometry: {
        type: "Polygon",
        coordinates: [mesh.coordinates],
      },
      properties: {
        population: mesh.PT00_2024,
        meshId: mesh.MESH_ID,
        color: getPopulationColor(mesh.PT00_2024),
      },
    };
  });

  return {
    type: "FeatureCollection",
    features,
  };
}

土地利用データの構造を理解させるように指示をする

つづいて土地利用についても、正しく読み込めていなかったので中身の確認を指示

土地利用が正しく表示されない landUsePathsのファイルの中身を確認。以下のコードごとに対象が想像しやすいようにわかりやすく色分けしてほしい

コード	種別	定義
0100	田	湿田・乾田・沼田・蓮田及び田とする。
0200	その他の農用地	麦・陸稲・野菜・草地・芝地・りんご・梨・桃・ブドウ・茶・桐・はぜ・こうぞ・しゅろ等を栽培する土地とする。
0300	-	-
0400	-	-
0500	森林	多年生植物の密生している地域とする。
0600	荒地	しの地・荒地・がけ・岩・万年雪・湿地・採鉱地等で旧土地利用データが荒地であるところとする。
0700	建物用地	住宅地・市街地等で建物が密集しているところとする。
0800	-	-
0901	道路	道路などで、面的に捉えられるものとする。
0902	鉄道	鉄道・操車場などで、面的にとらえられるものとする。
1000	その他の用地	運動競技場、空港、競馬場・野球場・学校・港湾地区・人工造成地の空地等とする。
1100	河川地及び湖沼	人工湖・自然湖・池・養魚場等で平水時に常に水を湛えているところ及び河川・河川区域の河川敷とする。
1200	-	-
1300	-	-
1400	海浜	海岸に接する砂、れき、岩の区域とする。
1500	海水域	隠顕岩、干潟、シーパースも海に含める。
1600	ゴルフ場	ゴルフ場のゴルフコースの集まっている部分のフェアウエイ及びラフの外側と森林の境目を境界とする。
/src/assets/data/L03-a-21_5436-jgd2011_GML/L03-a-21_5436.geojson これらのファイルの中身を確認して本当にこの表示方法でただしいか再度確認してください

結果、下記のように文句ないParseを作成してくれました

export async function mergeLandUseData(paths: string[]): Promise<LandUseMeshData[]> {
  try {
    const dataPromises = paths.map((path) => fetch(path).then((res) => res.json()));
    const results = await Promise.all(dataPromises);

    return results.flatMap((result) =>
      result.features.map((feature: Feature<Polygon>) => ({
        L03b_001: feature.properties?.L03b_001 || "",
        L03b_002: feature.properties?.L03b_002 || "",
        L03b_003: feature.properties?.L03b_003 || "",
      })),
    );
  } catch (error) {
    console.error("Error merging land use data:", error);
    return [];
  }
}

export function meshIdToLatLng(meshId: string): [number, number] {
  console.log("Converting mesh ID to lat/lng:", meshId);

  // メッシュコードの各部分を分解
  const lat1 = Math.floor(Number(meshId.substring(0, 2)) / 1.5);
  const lng1 = Number(meshId.substring(2, 4));
  const lat2 = Number(meshId.substring(4, 5)) / 8;
  const lng2 = Number(meshId.substring(5, 6)) / 8;
  const lat3 = Number(meshId.substring(6, 7)) / 80;
  const lng3 = Number(meshId.substring(7, 8)) / 80;

  // 石川県の中心座標(金沢市)を基準に調整
  const baseLng = 136.6; // 金沢市の経度
  const baseLat = 36.5; // 金沢市の緯度

  const result: [number, number] = [
    baseLng + (lng1 + lng2 + lng3 - 36), // 経度(金沢を基準に調整)
    baseLat + (lat1 + lat2 + lat3 - 36), // 緯度(金沢を基準に調整)
  ];

  console.log("Mesh conversion details:", {
    meshId,
    components: {
      lat1,
      lng1,
      lat2,
      lng2,
      lat3,
      lng3,
    },
    baseCoordinates: {
      baseLng,
      baseLat,
    },
    result,
  });

  return result;
}

export function createMeshPolygon(meshId: string): number[][] {
  const [lng, lat] = meshIdToLatLng(meshId);
  const meshSize = 1 / 80;

  return [
    [lng, lat],
    [lng + meshSize, lat],
    [lng + meshSize, lat + meshSize],
    [lng, lat + meshSize],
    [lng, lat],
  ];
}
いいですね。 これでCommitしてください

以下のようにコメント付きでcommit実行してくれます。

(base) disasterAoIAnalysis % git add src/components/DisasterAOIAnalyze.vue && git commit -m "fix: 土地利用デ
ータの表示方法を修正 - 各メッgit add src/components/DisasterAOIAnalyze.vue && git commit -m "fix: 土地利用デ
ータの表示方法を修正 - 各メッシュの主要な土地利用タイプに基づいて色分けを実装"

撮像エリア提案部分のコンポーネント作成指示

ここまでのデータを統合して、災害時に広いエリアの中から撮像エリアの候補を提案する機能を追加したいです。
* 撮像候補を複数提案してくれて、それぞれの理由を説明してくれるAgent
* データは災害エリア、人口データ、土地利用データ、学校、病院をりようする
 * 衛星が一回に撮像できるサイズは縦横10kmの正方形とします。

これで新しいvue componentと、スコア計算の大枠は作成してくれました。
ただ結果は表示されないので下記のような追加プロンプトで一緒にデバッグします。

提案されないので、 計算過程をデバッグ出力してください
分析をするときに、データの読み込みが完了していないようです。

ついに下記の、各種データからスコアを計算して提案エリアを生成することができました。
(今回災害予想エリアはデータとしては簡易的にしか設定しておりません。すみません。)

スコアのどれを優先するかで候補が変わる様子がシミュレーションできました。

実際には、下記のコードのなかの係数を調整してさらに実用的な候補を提案できるようにする必要があります。

function calculateAreaScore(
  bounds: [[number, number], [number, number]],
  disasterData: FeatureCollection,
  populationData: FeatureCollection,
  landUseData: FeatureCollection,
  schoolData: FeatureCollection,
  medicalData: FeatureCollection,
): { score: number; reasons: string[] } {
  const reasons: string[] = [];
  let score = 0;

  console.log("\n=== Calculating score for area ===");
  console.log("Bounds:", bounds);

  // 災害の深刻度を評価
  const disasterFeatures = disasterData.features.filter((feature) => {
    if (feature.geometry.type === "Polygon") {
      const coordinates = feature.geometry.coordinates[0];
      return coordinates.some((coord) => coord[0] >= bounds[0][0] && coord[0] <= bounds[1][0] && coord[1] >= bounds[0][1] && coord[1] <= bounds[1][1]);
    }
    return false;
  });

  console.log("Disaster features in bounds:", disasterFeatures.length);
  console.log(
    "Disaster features:",
    disasterFeatures.map((f) => ({
      level: f.properties?.level,
      type: f.geometry.type,
    })),
  );

  if (disasterFeatures.length > 0) {
    // 災害切迫エリアのスコアを大幅に増加
    const criticalCount = disasterFeatures.filter((f) => f.properties?.level === "critical").length;
    const dangerCount = disasterFeatures.filter((f) => f.properties?.level === "danger").length;

    // 災害切迫エリアは1箇所で20点、危険エリアは1箇所で10点
    score += criticalCount * 20 + dangerCount * 10;

    if (criticalCount > 0) {
      reasons.push(`災害切迫エリアが${criticalCount}箇所存在(スコア: ${criticalCount * 20}点)`);
    }
    if (dangerCount > 0) {
      reasons.push(`危険エリアが${dangerCount}箇所存在(スコア: ${dangerCount * 10}点)`);
    }

    console.log("Critical disaster areas:", criticalCount);
    console.log("Danger areas:", dangerCount);
  }

  // 人口密度を評価(スコアを調整)
  const populationFeatures = populationData.features.filter((feature) => {
    if (feature.geometry.type === "Polygon") {
      const coordinates = feature.geometry.coordinates[0];
      return coordinates.some((coord) => coord[0] >= bounds[0][0] && coord[0] <= bounds[1][0] && coord[1] >= bounds[0][1] && coord[1] <= bounds[1][1]);
    }
    return false;
  });

  const totalPopulation = populationFeatures.reduce((sum, feature) => sum + (feature.properties?.population || 0), 0);
  console.log("Population features in bounds:", populationFeatures.length);
  console.log("Total population:", totalPopulation);
  console.log(
    "Population features:",
    populationFeatures.map((f) => ({
      population: f.properties?.population,
      type: f.geometry.type,
    })),
  );

  if (totalPopulation > 0) {
    // 人口スコアを調整(最大5点)
    score += totalPopulation / 250;
    reasons.push(`人口${totalPopulation.toLocaleString()}人が存在(スコア: ${(totalPopulation / 250).toFixed(1)}点)`);
  }

  // 重要施設を評価(スコアを調整)
  const schoolCount = schoolData.features.filter((feature) => isFeatureInBounds(feature, bounds)).length;
  const medicalCount = medicalData.features.filter((feature) => isFeatureInBounds(feature, bounds)).length;
  console.log("Schools in bounds:", schoolCount);
  console.log("Medical facilities in bounds:", medicalCount);

  if (schoolCount > 0 || medicalCount > 0) {
    // 重要施設のスコアを調整(1施設あたり1点、最大5点)
    const facilityScore = (schoolCount + medicalCount) * 2;
    score += facilityScore;
    reasons.push(`学校${schoolCount}校、医療機関${medicalCount}箇所が存在(スコア: ${facilityScore}点)`);
  }

  // 土地利用の重要性を評価(スコアを調整)
  const landUseFeatures = landUseData.features.filter((feature) => {
    if (feature.geometry.type === "Polygon") {
      const coordinates = feature.geometry.coordinates[0];
      return coordinates.some((coord) => coord[0] >= bounds[0][0] && coord[0] <= bounds[1][0] && coord[1] >= bounds[0][1] && coord[1] <= bounds[1][1]);
    }
    return false;
  });

  console.log("Land use features in bounds:", landUseFeatures.length);
  console.log(
    "Land use types:",
    landUseFeatures.map((f) => f.properties?.type),
  );

  const importantLandUse = landUseFeatures.filter((feature) => ["建物用地", "道路", "鉄道"].includes(feature.properties?.type || "")).length;

  console.log("Important land use features:", importantLandUse);

  if (importantLandUse > 0) {
    // 重要インフラのスコアを調整(1箇所あたり0.5点、最大5点)
    const landUseScore = Math.min(importantLandUse * 4, 20);
    score += landUseScore;
    reasons.push(`重要インフラが${importantLandUse}箇所存在(スコア: ${landUseScore.toFixed(1)}点)`);
  }

  console.log("Final score:", score);
  console.log("Reasons:", reasons);

  return { score, reasons };
}

まとめ、今後の展開

実作業時間としては2日 程度でいままであまり触っていなかったGeoJson、XMLの表示も実現できました。これはAI Editorのサポートなしでは正直5-10倍程度時間がかかっていたと思います。

実際の災害提案システムとしても、いかのように拡張をすれば十分役立っていく感触はつかめました。

  • 全国のインフラデータの事前準備と、災害予報入力方法対応
  • 自動タスキングとの接続
    せっかくなので人間の入力に対して反応するのではなく、自律的に情報をWatchして、Agentとして動作する形にしていきたいとおもいます。

こういった国土情報、インフラ情報の 国交省の公開データがとても充実しており、AI Editorを利用するとその視覚化、データとしての活用もすぐにできるということが実証できたので、もっと活用事例を増やしていきたいです。

うまくいかなかった指示について

  • linter の設定を厳しくしていると、Agentの生成するコードがエラーになる。 そのエラーをまとめて修正してほしいという指示をしたが、なかなか修正がすすまない。(プロジェクトのルール設定等で改善されるか試したい)

  • xmlの読み込み、parse処理のエラーログを貼り付けて、なおしてという指示。 Agentが頑張ってgrepしたり、構造を読み進めて処理をしてくれたが結局は中途半端な状態でおわる。 自分でしっかりとXMLの構造を理解してその構造にそったParse処理を指示することで解決した。geoJsonの場合はAgentまかせでParseがうまくすすんだが、xmlは構造が自由すぎるので、ある程度自分でもデータ構造を理解して指示をしないとRequestの無駄遣いになる。

  • データの単位の解釈を勝手に進めてしまい、 人口データが平均になっているので小数点がついていたところを 1000人単位と勘違いして1000倍してしまい、とても人口が多い状態になってしまった。

その他痛感したこと

  • 人間側がボトルネック モデルの性能よりも指示プロンプトの性能の差がすごい
  • モデルのベンチマークよりも、指示プロンプトをどの程度柔軟にだせるかの エンジニアのベンチマークテストを作成するほうが重要だ
シンギュラリティ・ソサエティ

Discussion