📖

座標点群配列から、各点の近傍(半径100m以内)の点群数を数える。座標点群配列に数えた点群数と住所を追加する。

に公開

本記事の分類

  • 学習ノート

機能

  • 舗装修繕箇所(ポットホールの修繕箇所)の座標データから定量的に舗装修繕回数が多い範囲(部分)を評価する
  • 市町名及び町丁目情報を取得する

想定シーン

  • 舗装修繕箇所の優先順位付けの参考資料を作成する

仕様

  • 各舗装補修箇所の近傍(半径100m以内)の補修箇所数を数える。
  • 各点の座標データと補修箇所数を要素とする新しい配列を作成する。
  • ラフな精度でも問題ないため地球を球体として計算している。

参考

Code

  • 以下のとおり
createStaffTable

// ==================================================
// Google Apps Script: GSI逆ジオコーダーのみ版(ZipCloud不要)+負荷制御+一括処理
// ==================================================

// ■ 定数設定 ■
const API_BASE_GSI = 'https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress';
const BATCH_SIZE   = 10;   // 同時並列リクエスト件数
const QPS_LIMIT    = 5;    // 1秒あたり最大リクエスト数
const BATCH_PER_RUN = 50;  // 1実行あたり処理ポイント数
const RADIUS_KM    = 0.1;  // 近傍判定半径(km)
const EARTH_RADIUS_KM = 6371;  // 地球半径(km)

// ★ 栃木県の対象市町村マッピング表(muniCd → 市町村名)★
const muniCdMap = {
  "09210": "大田原市",
  "09213": "那須塩原市",
  "09407": "那須町"
};

// ■ 1) シート入力取得&正規化 ■
function fetchRawInputs() {
  return SpreadsheetApp.getActiveRange().getValues().map(row => {
    const s = String(row[0]).trim();
    if (s.includes(',')) {
      return s; // 座標
    }
    return ''; // 郵便番号データは無視する(空文字にする)
  }).filter(v => v); // 空白除外
}

// ■ 2) 距離計算ユーティリティ ■
function toRadians(deg) {
  return deg * Math.PI / 180;
}
function calculateDistance(lat1, lon1, lat2, lon2) {
  const φ1 = toRadians(lat1), φ2 = toRadians(lat2);
  const λ1 = toRadians(lon1), λ2 = toRadians(lon2);
  const cosθ = Math.cos(φ1) * Math.cos(φ2) * Math.cos(λ1 - λ2) + Math.sin(φ1) * Math.sin(φ2);
  return EARTH_RADIUS_KM * Math.acos(cosθ);
}

function summarizeNearby(points) {
  return points.map(pt => {
    const [lat1, lon1] = pt;
    let count = points.filter(q => {
      const [lat2, lon2] = q;
      if (lat1 === lat2 && lon1 === lon2) return false; // 自分自身はスキップ
      return calculateDistance(lat1, lon1, lat2, lon2) <= RADIUS_KM;
    }).length;
    count++;
    return [count, lat1, lon1];
  });
}

// ■ 4) GSI逆ジオコーダーAPIバッチ取得(市名マッピングあり)■
function fetchGsiAddressesInBatches(coords) {
  const out = [];
  for (let i = 0; i < coords.length; i += BATCH_SIZE) {
    const batch = coords.slice(i, i + BATCH_SIZE);
    const reqs = batch.map(([lat, lon]) => ({
      url: `${API_BASE_GSI}?lat=${lat}&lon=${lon}`,
      muteHttpExceptions: true
    }));
    const resps = UrlFetchApp.fetchAll(reqs);
    resps.forEach(r => {
      let city = '', town = '';
      try {
        const j = JSON.parse(r.getContentText());
        if (j.results) {
          const results = j.results;
          const muniCd = results.muniCd;
          const lv01Nm = results.lv01Nm || '';
          city = muniCd ? (muniCdMap[muniCd] || `コード${muniCd}`) : '';
          town = lv01Nm;
        }
      } catch (e) { /* ignore */ }
      out.push({ city, town });
    });
    Utilities.sleep(Math.ceil(batch.length / QPS_LIMIT) * 1000);
  }
  return out;
}

// ■ 5) プロパティ操作(進捗保存)■
function getLastIndex() {
  return Number(PropertiesService.getScriptProperties().getProperty('LAST_INDEX') || 0);
}
function setLastIndex(idx) {
  PropertiesService.getScriptProperties().setProperty('LAST_INDEX', String(idx));
}

// ■ 6) ソート関数(降順)■
function sortResultSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('result');
  if (!sh) return;
  const data = sh.getDataRange().getValues();
  if (data.length <= 1) return;
  const [header, ...body] = data;
  body.sort((a, b) => (b[0] || 0) - (a[0] || 0));
  sh.clear();
  sh.getRange(1, 1, 1, header.length).setValues([header]);
  sh.getRange(2, 1, body.length, header.length).setValues(body);
}

// ■ 7) バッチ処理メイン(続きありならtrue、終わったらfalse)■
function processBatch() {
  const raw = fetchRawInputs();
  const total = raw.length;
  let idx = getLastIndex();

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('result') || ss.insertSheet('result');
  if (idx === 0) {
    sh.clear();
    sh.getRange(1, 1, 1, 5).setValues([['R100m内補修回数', '緯度', '経度', '市町', '町丁目']]);
  }

  const end = Math.min(idx + BATCH_PER_RUN, total);
  const sliceRaw = raw.slice(idx, end);

  const coords = sliceRaw.map(v => v.split(',').map(Number));

  const summary = summarizeNearby(coords);
  const gsiAddrs = fetchGsiAddressesInBatches(coords);

  const rows = [];
  for (let i = 0; i < coords.length; i++) {
    const [count, lat, lon] = summary[i];
    const { city, town } = gsiAddrs[i];
    rows.push([count, lat, lon, city, town]);
  }

  sh.getRange(idx + 2, 1, rows.length, 5).setValues(rows);

  if (end < total) {
    setLastIndex(end);
    return true; // 続きあり
  } else {
    sortResultSheet();
    PropertiesService.getScriptProperties().deleteProperty('LAST_INDEX');
    return false; // 終了
  }
}

// ■ 8) 全部一括実行用メイン(whileで全部やる)■
function main() {
  while (true) {
    const continueProcessing = processBatch();
    if (!continueProcessing) break;
  }
}

使い方

  • googleSpreadSheetに座標値を入力(1列目は緯度、2列目は経度)
  • 座標値を範囲選択
  • ”拡張機能”→”Apps Script”を選択
  • 上記codeを入力しmain関数を実行

Google map(My map)

  • Google map (My map) に反映した例

参考にしたwebPage

Discussion