サンプル

位置から空間アドレスを求める

経度・緯度・楕円体高とレベルを入れると、その地点を含むセルの空間アドレス、セルの番号、セルの中心を表示します。空間アドレスからセルの番号と中心に戻すこともできます。

新しいタブで開く

コード

ビルドせずにブラウザで読み込める形で書いています。index.html の import map で、@mirgrid/web-sdk をこのサイトに置いた SDK に結び付けています。npm でインストールしてバンドラを使うプロジェクトでは、import map は要りません。

main.js

import init, {
  spatial_geodetic_to_ecef,
  spatial_ecef_to_voxel,
  spatial_hilbert_cellid_hex,
  spatial_voxel_size_m,
  spatial_voxel_to_ecef,
  spatial_hilbert_cellid_decode,
  spatial_cellid_to_ecef,
  spatial_ecef_to_llh_rad,
  spatial_ecef_height,
  spatial_ecef_to_enu,
} from '@mirgrid/web-sdk';

await init();

const $ = (id) => document.getElementById(id);
const DEG = 180 / Math.PI;
const num = (value, digits) => value.toLocaleString('ja-JP', { maximumFractionDigits: digits });

// セルの一辺を読みやすい単位で書く(レベル 1 は約 8,389 km、レベル 30 は約 1.6 cm)
function formatSize(meters) {
  if (meters >= 1000) return `${num(meters / 1000, 3)} km`;
  if (meters >= 1) return `${num(meters, 3)} m`;
  return `${num(meters * 100, 4)} cm`;
}

// ECEF の点を「経度・緯度・楕円体高」の 3 行にする
function formatLlh([x, y, z]) {
  const { lon, lat } = spatial_ecef_to_llh_rad(x, y, z);
  return `経度 ${(lon * DEG).toFixed(8)}\n緯度 ${(lat * DEG).toFixed(8)}\n楕円体高 ${spatial_ecef_height(x, y, z).toFixed(3)} m`;
}

// 1 つ粗いレベルと同じ桁(s)、新しく決まった桁(d)、末尾の 0(z) に分けて色を付ける
function hexHtml(hex, prev) {
  const zeros = hex.length - hex.replace(/0+$/, '').length;
  const kind = (i) => (i >= hex.length - zeros ? 'z' : !prev ? 'p' : prev[i] === hex[i] ? 's' : 'd');
  let html = '';
  for (let i = 0; i < hex.length; ) {
    let j = i;
    while (j < hex.length && kind(j) === kind(i)) j++;
    html += `<span class="${kind(i)}">${hex.slice(i, j)}</span>`;
    i = j;
  }
  return html;
}

for (let level = 1; level <= 30; level++) {
  $('level').add(new Option(`${level}(一辺 ${formatSize(spatial_voxel_size_m(level))})`, level, false, level === 24));
}

// 入力に問題があればその説明を、なければ空文字を返す
function checkPoint([lon, lat, height]) {
  // 空欄や数でない入力は valueAsNumber が NaN になる
  if (![lon, lat, height].every(Number.isFinite)) return '経度・緯度・楕円体高に数を入れてください。';
  if (Math.abs(lon) > 180) return '経度は −180〜180 の範囲で入れてください。';
  if (Math.abs(lat) > 90) return '緯度は −90〜90 の範囲で入れてください。';
  return '';
}

// 同じ地点を全レベルで変換しておく(表と結果の両方で使う)
function addressesAt(point) {
  const [x, y, z] = spatial_geodetic_to_ecef(...point);
  return Array.from({ length: 30 }, (_, i) => {
    const level = i + 1;
    const [vx, vy, vz] = spatial_ecef_to_voxel(x, y, z, level);
    return { level, vx, vy, vz, hex: spatial_hilbert_cellid_hex(vx, vy, vz, level) };
  });
}

function showError(id, message) {
  $(`${id}-error`).textContent = message;
  $(`${id}-error`).hidden = !message;
  $(id).hidden = Boolean(message);
}

function update() {
  const point = ['lon', 'lat', 'height'].map((id) => $(id).valueAsNumber);
  let addresses;
  let problem = checkPoint(point);
  if (!problem) {
    try {
      addresses = addressesAt(point);
    } catch {
      // 格子は地球を包む立方体なので、それより外(極端に高い・深い地点)はセルにできない
      problem = 'この地点は格子の範囲の外にあります。楕円体高を確かめてください。';
    }
  }
  showError('forward', problem);
  if (problem) {
    $('rows').replaceChildren();
    return;
  }

  const level = Number($('level').value);
  const { vx, vy, vz, hex } = addresses[level - 1];
  const center = spatial_voxel_to_ecef(vx, vy, vz, level);
  // 入れた地点を原点にした ENU に直すと、東・北・上のずれ(m)がそのまま読める
  const [east, north, up] = spatial_ecef_to_enu(...center, ...point);

  $('address').textContent = hex;
  $('voxel').textContent = `${vx}, ${vy}, ${vz}`;
  $('size').textContent = formatSize(spatial_voxel_size_m(level));
  $('center').textContent = formatLlh(center);
  $('distance').textContent = `${num(Math.hypot(east, north, up), 3)} m\n(東 ${num(east, 3)} m、北 ${num(north, 3)} m、上 ${num(up, 3)} m)`;

  $('rows').innerHTML = addresses
    .map(
      (row, i) =>
        `<tr data-level="${row.level}" aria-current="${row.level === level}"><td>${row.level}</td><td>${formatSize(spatial_voxel_size_m(row.level))}</td><td class="hex">${hexHtml(row.hex, addresses[i - 1]?.hex)}</td></tr>`,
    )
    .join('');

  $('reverse-input').value = hex;
  decode();
}

function decode() {
  const hex = $('reverse-input').value.trim().toLowerCase();
  if (!/^[0-9a-f]{32}$/.test(hex)) {
    showError('reverse', '空間アドレスは 0〜9 と a〜f の 32 文字で入れてください。');
    return;
  }
  try {
    const { vx, vy, vz, level } = spatial_hilbert_cellid_decode(hex);
    $('reverse-level').textContent = `${level}(一辺 ${formatSize(spatial_voxel_size_m(level))})`;
    $('reverse-voxel').textContent = `${vx}, ${vy}, ${vz}`;
    $('reverse-center').textContent = formatLlh(spatial_cellid_to_ecef(hex));
    showError('reverse', '');
  } catch {
    showError('reverse', 'この空間アドレスはセルに戻せません。値を確かめてください。');
  }
}

for (const id of ['lon', 'lat', 'height', 'level']) $(id).addEventListener('input', update);
$('reverse-input').addEventListener('input', decode);
$('rows').addEventListener('click', (event) => {
  const row = event.target.closest('tr');
  if (!row) return;
  $('level').value = row.dataset.level;
  update();
});

update();

index.html

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>位置から空間アドレスを求める</title>
<link rel="stylesheet" href="/assets/sample.css">
<script type="importmap">
{ "imports": { "@mirgrid/web-sdk": "/sdk/web-sdk/voxel_core.js", "@mirgrid/web-sdk/": "/sdk/web-sdk/" } }
</script>
<script type="module" src="main.js"></script>
<style>
  .column { display: grid; gap: 16px; align-content: start; }
  @media (min-width: 900px) {
    .app { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
  }
  .address { margin: 4px 0 12px; font: 20px/1.3 var(--mono); color: var(--navy); overflow-wrap: anywhere; }
  #reverse, #reverse-error { margin-top: 12px; }
  .facts dd { white-space: pre-line; }
  .levels table { font-size: 12.5px; }
  .levels th, .levels td { padding: 4px 6px; white-space: nowrap; }
  .levels td:nth-child(2) { color: var(--muted); }
  .levels tbody tr { cursor: pointer; }
  .levels tbody tr:hover { background: var(--film); }
  .levels tbody tr[aria-current="true"] { background: var(--tint); }
  .hex { font-family: var(--mono); }
  .hex .s { color: var(--faint); }
  .hex .d { color: var(--link); font-weight: 600; }
  .hex .z { color: #aab4c3; }
  .hex .p { color: var(--navy); }
  @media (max-width: 420px) {
    .levels th, .levels td { padding: 4px 3px; white-space: normal; }
    .levels .hex { font-size: 11px; letter-spacing: -0.02em; white-space: nowrap; }
    .address { font-size: 15px; }
  }
</style>
</head>
<body>
<main class="app">
  <div class="column">
  <section class="panel">
    <h1>地点とレベル</h1>
    <div class="fields">
      <label>経度(度)<input id="lon" type="number" step="any" value="139.7671248"></label>
      <label>緯度(度)<input id="lat" type="number" step="any" value="35.6812362"></label>
      <label>楕円体高(m)<input id="height" type="number" step="any" value="40"></label>
      <label>レベル<select id="level"></select></label>
    </div>
    <p class="note">初期値は東京駅のあたりです。値を変えると、すぐに結果が変わります。</p>

    <h2>結果</h2>
    <p id="forward-error" class="error" hidden></p>
    <div id="forward">
      <div class="status">空間アドレス</div>
      <div id="address" class="address"></div>
      <dl class="facts">
        <dt>セルの番号</dt><dd id="voxel"></dd>
        <dt>セルの一辺</dt><dd id="size"></dd>
        <dt>セルの中心</dt><dd id="center"></dd>
        <dt>中心までの距離</dt><dd id="distance"></dd>
      </dl>
      <p class="note">距離は、入れた地点を原点にした ENU(東・北・上)で測っています。</p>
    </div>
  </section>

  <section class="panel">
    <h2>空間アドレスから戻す</h2>
    <label>空間アドレス(32 桁の 16 進数)<input id="reverse-input" class="mono" spellcheck="false" autocomplete="off" maxlength="32"></label>
    <p id="reverse-error" class="error" hidden></p>
    <dl id="reverse" class="facts">
      <dt>レベル</dt><dd id="reverse-level"></dd>
      <dt>セルの番号</dt><dd id="reverse-voxel"></dd>
      <dt>セルの中心</dt><dd id="reverse-center"></dd>
    </dl>
  </section>
  </div>

  <section class="panel levels">
    <h2>同じ地点のレベルごとの空間アドレス</h2>
    <p class="note">青い桁は、1 つ上のレベルから新しく決まった桁です。行を押すと、そのレベルを選びます。</p>
    <table>
      <thead><tr><th scope="col">レベル</th><th scope="col">セルの一辺</th><th scope="col">空間アドレス</th></tr></thead>
      <tbody id="rows"></tbody>
    </table>
  </section>
</main>
</body>
</html>