ドキュメント / 使い方

セルの集合

CellSetJs は、同じレベルのセルを集めたものです。点群や建物の形をセルの集合にしておくと、重なりを調べたり、距離を測ったり、2 つの時点の違いを見つけたりできます。この文書では、集合の作り方と主な計算を説明します。

以下の例は、最初に次のように読み込んだものとして書いています。Node.js で試すときは、init() の呼び方をはじめにのとおりに変えてください。

import init, {
  CellSetJs,
  ops_cells_in_roi,
  ops_cell_relation,
  ops_diff3,
  ops_extrude_polygon_to_cells,
  spatial_geodetic_to_ecef,
  spatial_ecef_to_voxel,
  spatial_enu_to_ecef,
  spatial_voxel_size_m,
} from '@mirgrid/web-sdk';

await init();

集合を作る

集合の中身は WebAssembly のメモリにあります。使い終わったら free() で解放してください(使い終わったら解放するを参照)。

点から作る

CellSetJs.from_ecef_points に、レベルと ECEF の点の並び [x, y, z, x, y, z, …] を渡します。それぞれの点が入るセルが集合になります。同じセルに入る点が複数あっても、セルは 1 つだけです。

const tokyo = spatial_geodetic_to_ecef(139.767125, 35.681236, 40);
const tokyoEast = spatial_geodetic_to_ecef(139.767135, 35.681236, 40); // 約 0.9 m 東
const fromPoints = CellSetJs.from_ecef_points(24, new Float64Array([...tokyo, ...tokyoEast]));
console.log(fromPoints.level, fromPoints.size);
console.log(fromPoints.cell_ids());
24 2
[
  '0000000002e9380374bc9e7c5e140000',
  '0000000002e9380374bc9e7c5ef40000'
]

level はレベル、size はセルの数です。cell_ids() は空間アドレスの配列を返します。

セルの番号から作る

CellSetJs.from_indices には、セルの番号の並び [vx, vy, vz, vx, vy, vz, …] を渡します。同じ番号が 2 回出てきたときは、最初のものだけが残ります。集合は、入れた順番を覚えています。

次の例では、東京駅のあたりのセルを起点にして、厚さ 1 セルの板を 2 枚作ります。この文書のほかの例でも、この板を使います。

const [vx, vy, vz] = spatial_ecef_to_voxel(tokyo[0], tokyo[1], tokyo[2], 24);

// 番号の x を x0 から count 個、y を 3 個並べた板を作ります
function plate(x0, count) {
  const flat = [];
  for (let dx = 0; dx < count; dx++) {
    for (let dy = 0; dy < 3; dy++) flat.push(vx + x0 + dx, vy + dy, vz);
  }
  return CellSetJs.from_indices(24, new Float64Array(flat));
}

const a = plate(0, 3); // x が 0〜2 の 9 セル
const b = plate(2, 3); // x が 2〜4 の 9 セル
console.log(a.size, b.size);
9 9

空の集合は new CellSetJs(level) で作れます。

中身を読む

集合からは、次のような値を読めます。

メソッド 返すもの
indices() セルの番号を [vx, vy, vz, …] と並べた Float64Array(入れた順)
cell_ids() 空間アドレスの配列(入れた順)
contains(vx, vy, vz) そのセルが集合に入っているかどうか
bounds_ecef() すべてのセルを囲む ECEF の範囲 [最小の x, 最小の y, 最小の z, 最大の x, 最大の y, 最大の z]
centers_enu(lon, lat, h) セルの中心を、指定した原点の ENU に変換した Float32Array(描画用)
area_m2() 表に出ている面の面積の合計(m²)
console.log(a.contains(vx, vy, vz), a.contains(vx + 5, vy, vz));
console.log(a.area_m2());
true false
30

area_m2() は、隣のセルと接している面を数えません。板 a は 3 × 3 × 1 セルなので、上下の面が 9 m² ずつ、側面が 12 m² で、合計 30 m² になります。

和・積・差

2 つの集合の和は union、積(共通部分)は intersect、差は difference で求めます。どれも新しい集合を返し、元の集合は変わりません。

const union = a.union(b);
const both = a.intersect(b);
const onlyA = a.difference(b);
console.log(union.size, both.size, onlyA.size);
15 3 6

2 つの集合のレベルは揃えておく必要があります。レベルが違うと Error になります(粗いレベルにするを参照)。

近くのセルを共有する

a.shared_with(other, tolerance) は、a のセルのうち、other のセルが近くにあるものを返します。「近く」とは、番号の差が x・y・z のどれも tolerance 以下であることです。tolerance には 0〜8 を指定できます。0 なら、intersect と同じく同じセルだけを見ます。

点群どうしを比べると、同じ壁でもセル 1 つぶんずれることがあります。tolerance を 1 にしておくと、こうした小さなずれを許して比べられます。

const c = plate(5, 1); // x が 5 の 3 セル。板 a との間に 2 セルぶんの隙間があります
const near2 = a.shared_with(c, 2);
const near3 = a.shared_with(c, 3);
console.log(near2.size, near3.size);
0 3

距離を測る

a.distance_m(other, maxSteps) は、2 つの集合の間の隙間の長さを m で返します。重なっているときや、隙間なく接しているときは 0 です。

console.log(a.distance_m(c, 10));
console.log(a.distance_m(b, 10));
2
0

計算では、a を 1 セルずつ広げていき、other に届いたところで止めます。maxSteps セル広げても届かないときは null を返します。どちらかの集合が空のときも null です。

const d = plate(20, 1); // 板 a から 17 セル離れた板
console.log(a.distance_m(d, 10));
console.log(a.distance_m(d, 30));
null
17

null は「maxSteps の範囲では答えが出なかった」という意味で、距離が 0 という意味ではありません。結果を if (!distance) のように調べると、0 と null の区別がつかなくなります。distance === null で調べてください。

maxSteps を大きくするほど計算に時間がかかります。知りたい距離の範囲に合わせて決めてください。また、セルの一辺より細かい隙間は測れません。

粗いレベルにする

downshift(level) は、集合を粗いレベルに変換した新しい集合を返します。細かいセルは、それを含む粗いセルにまとまります。今より細かいレベルを指定したときは、今のレベルのままです。

const coarse = a.downshift(22);
console.log(coarse.level, coarse.size);
try {
  a.union(coarse);
} catch (error) {
  console.log(error.message);
}
22 2
union: levels differ (L24 vs L22); downshift first

レベルの違う集合を比べるときは、細かいほうを downshift で粗いほうに揃えてから計算します。

範囲で切り出す

範囲(ROI)は、ECEF の座標(m)で書いた JavaScript のオブジェクトで指定します。書ける形は次のとおりです。

形 書き方
箱 { aabb: { min: [x, y, z], max: [x, y, z] } }
球 { sphere: { center: [x, y, z], radius_m: 半径 } }
平面に挟まれた層 { slab: { origin: [x, y, z], normal: [x, y, z], half_thickness_m: 厚さの半分 } }
範囲の外側 { not: 範囲 }
すべてに入る部分 { and: [範囲, 範囲, …] }
どれかに入る部分 { or: [範囲, 範囲, …] }

箱の辺は ECEF の軸に沿っているので、地面に対しては傾いています。地面と平行な層が欲しいときは、slab の normal に、その場所の真上の向きを指定します。

範囲に入るセルを集める

ops_cells_in_roi(roi, level, maxCells) は、範囲に掛かるセルをすべて集めた集合を返します。次の例では、東京駅のあたりの半径 2 m の球に掛かるセルを集めています。

const sphere = { sphere: { center: tokyo, radius_m: 2 } };
const nearTokyo = ops_cells_in_roi(sphere, 24, 10000);
console.log(nearTokyo.size);
87

maxCells は、調べるセルの数の上限です。範囲を囲む箱に入るセルの数がこれを超えると、計算を始める前に Error になります。

try {
  ops_cells_in_roi(sphere, 24, 100);
} catch (error) {
  console.log(error.message);
}
走査候補セル数 343 が上限 100 を超えています

ops_cells_in_roi には、大きさに限りのある範囲を渡してください。{ not: … } だけのように果てのない範囲は Error になります。

範囲を組み合わせることもできます。次の例は、球のうち、高さ 39.5〜40.5 m の層に入る部分です。

const up = spatial_enu_to_ecef(0, 0, 1, 139.767125, 35.681236, 40);
const layer = {
  slab: {
    origin: tokyo,
    normal: [up[0] - tokyo[0], up[1] - tokyo[1], up[2] - tokyo[2]],
    half_thickness_m: 0.5,
  },
};
const disk = ops_cells_in_roi({ and: [sphere, layer] }, 24, 10000);
console.log(disk.size);
56

集合を範囲で切り出す

手元の集合を範囲で切り出すときは clip(roi, mode) を使います。mode が 'touching' なら範囲に少しでも掛かるセルを、'inside' ならまるごと範囲に入るセルだけを残します。

const insideOnly = nearTokyo.clip(sphere, 'inside');
console.log(insideOnly.size);
8

1 つのセルと範囲の関係を調べる

ops_cell_relation(roi, vx, vy, vz, level) は、セルが範囲の外にあるか('outside')、一部が掛かるか('intersects')、まるごと入るか('inside')を返します。

console.log(ops_cell_relation(sphere, vx, vy, vz, 24));
console.log(ops_cell_relation(sphere, vx + 2, vy, vz, 24));
console.log(ops_cell_relation(sphere, vx + 10, vy, vz, 24));
inside
intersects
outside

2 時点の差分をとる

ops_diff3(before, after, compareLevel, tolerance) は、2 つの時点の集合を比べて、セルを 3 つに分けます。

  • removed: 前の時点にはあって、後の時点には無いセル
  • added: 後の時点で新しく現れたセル
  • unchanged: どちらの時点にもあるセル

結果はオブジェクトで、removed・added・unchanged には番号の並び(Float64Array)が、removedCount・addedCount・unchangedCount にはその数が入ります。

tolerance を 1 以上にすると、番号の差が x・y・z のどれも tolerance 以下のセルを、同じものとみなします。対応づけは 1 対 1 です。まず同じセルどうしを組にし、残ったセルを近くのセルと組にします。組にできなかったセルが removed と added に入ります。

次の例では、前の時点の 4 セルのうち 1 つが、後の時点で隣のセルにずれています。後の時点には、離れた場所に新しいセルも 1 つ増えています。

const before = CellSetJs.from_indices(24, new Float64Array([
  vx, vy, vz,
  vx + 1, vy, vz,
  vx + 2, vy, vz,
  vx + 3, vy, vz,
]));
const after = CellSetJs.from_indices(24, new Float64Array([
  vx, vy + 1, vz, // 隣のセルにずれた
  vx + 1, vy, vz,
  vx + 2, vy, vz,
  vx + 3, vy, vz,
  vx + 8, vy, vz, // 新しく現れた
]));
const strict = ops_diff3(before, after, 24, 0);
console.log(strict.removedCount, strict.addedCount, strict.unchangedCount);
const loose = ops_diff3(before, after, 24, 1);
console.log(loose.removedCount, loose.addedCount, loose.unchangedCount);
1 2 3
0 1 4

tolerance が 0 のときは、ずれたセルが「消えたセル」と「現れたセル」の両方に数えられます。1 にすると、そのセルは変わらなかったものとして扱われます。

compareLevel に粗いレベルを指定すると、両方の集合をそのレベルに揃えてから比べます。入力より細かいレベルは指定できません。

多角形を押し出してセルにする

ops_extrude_polygon_to_cells は、平面に描いた多角形を一方向に押し出した立体を、セルの集合にします。部屋や敷地のような、図面から作った範囲をセルにするときに使います。

多角形の頂点は、原点(経度・緯度・楕円体高)から見た ENU の座標(m)で書きます。押し出す向きは 'up'(上)・'east'(東)・'north'(北)のどれかです。頂点の 2 つの値は、押し出す向きを除いた残りの 2 軸を、東・北・上の順に並べたものです。'up' なら(東, 北)、'north' なら(東, 上)、'east' なら(北, 上)になります。

次の例では、東京駅のあたりを原点に、東に 4 m・北に 3 m の床を、高さ 0〜2.5 m まで上に押し出しています。

const floor = new Float64Array([0, 0, 4, 0, 4, 3, 0, 3]);
const room = ops_extrude_polygon_to_cells(26, 139.767125, 35.681236, 40, floor, 0, 2.5, 100000, 'up');
console.log(room.size, room.size * spatial_voxel_size_m(26) ** 3);
1917 29.953125

引数は順に、レベル、原点の経度・緯度・楕円体高、多角形、高さの範囲の下と上(m)、調べるセルの数の上限、押し出す向きです。

中心が立体の中に入るセルだけを集めます。そのため、立体の縁にわずかに掛かるだけのセルは入りません。この例では、セルの体積の合計(約 29.95 m³)が、立体の体積(30 m³)とほぼ同じになっています。

使い終わったら解放する

CellSetJs の中身は WebAssembly のメモリにあり、JavaScript のガベージコレクタでは片づきません。使い終わった集合は free() で解放してください。解放しないと、ページを閉じるまでメモリに残ります。

union や clip などが返す集合も、それぞれ解放が必要です。

for (const set of [fromPoints, a, b, union, both, onlyA, c, near2, near3, d, coarse,
  nearTokyo, disk, insideOnly, before, after, room]) {
  set.free();
}

ops_diff3 の結果は普通の JavaScript のオブジェクトなので、解放は要りません。

関連する文書

セルの番号や空間アドレスの考え方は、位置と空間アドレスで説明しています。点群や 3D モデルから集合を作るときは、ファイルをセルにして表示するの toCellSet() を使います。関数の引数の一覧はAPI リファレンスにあります。