Skip to content
niryuu edited this page May 25, 2026 · 1 revision

render.ts — DOM 走査・遅延ロード・プラグイン

src/lib/render.ts (122 行)。embed.ts から起動される宣言的レンダリングの中核。

公開 API

export const renderGeoloniaMap: () => void;
export const registerPlugin: (plugin: (map: GeoloniaMap, target: HTMLElement, atts) => void) => void;

モジュールスコープ状態

const plugins: Array<(map: GeoloniaMap, target: HTMLElement, atts) => void> = [];

registerPlugin で push される唯一のグローバル配列。 renderGeoloniaMap 内のクロージャスコープに以下の state も:

  • isDOMContentLoaded: booleanDOMContentLoaded イベント受領フラグ
  • alreadyRenderedMaps: Array<{ map, target, atts }> — DOMContentLoaded 前に render されたマップのキュー
  • isRemoved: Symbol('map-is-removed')map[isRemoved] = true で破棄済みフラグを立てる

renderGeoloniaMap() の流れ (render.ts:12-114)

  1. PMTiles プロトコル登録: maplibregl.addProtocol('pmtiles', protocol.tile) (render.ts:13-14)
  2. iframe 許可チェック: checkPermission() (render.ts:16)。false なら何もせず console.error('[Geolonia] We are very sorry, but we cannot display our map in iframe.') で終了
  3. state 初期化: isDOMContentLoaded = falsealreadyRenderedMaps = []isRemoved = Symbol(...)
  4. keyring parse: keyring.parse() (render.ts:21) — script tag から API key と stage を抽出
  5. renderSingleMap 関数定義 (詳細は次節)
  6. DOMContentLoaded リスナー (render.ts:56-65)
    • isDOMContentLoaded = true
    • alreadyRenderedMaps の各 entry に対し、!map[isRemoved] なら全 plugin を実行
    • alreadyRenderedMaps.splice(0, length) で配列クリア
  7. IntersectionObserver 設定 (render.ts:67-80)
  8. API key 未指定警告: keyring.isGeoloniaStyle && !keyring.apiKey なら console.error('[Geolonia] Missing API key.') (render.ts:90-92)
  9. 即時 render: document.querySelectorAll('.geolonia[data-lazy-loading="off"]') をループ → renderSingleMap(container) (render.ts:95-102)
    • try/catch でラップ、エラー時は console.error(...) のみで次に進む
  10. lazy render: document.querySelectorAll('.geolonia:not([data-lazy-loading="off"])') を IntersectionObserver で監視開始 (render.ts:105-107)

renderSingleMap(target: HTMLElement) (render.ts:27-54)

.geolonia 要素に対して呼ばれる:

  1. const map = new GeoloniaMap(target) (render.ts:28)
  2. map.on('remove', () => { map[isRemoved] = true }) (render.ts:31-33)
  3. MutationObserver で container 削除を監視 (render.ts:37-45):
    const observer = new MutationObserver((mutationRecords) => {
      const removed = mutationRecords.some((record) =>
        [...record.removedNodes].some((node) => node === target),
      );
      if (removed && !map[isRemoved]) {
        map.remove();
      }
    });
    observer.observe(target.parentNode, { childList: true });
    • target.parentNode を観察するため、target 自身ではなく親の childList 変化で削除を検知する
    • target が削除されると removedNodes に含まれる
    • 親が消えた場合は検出不可 (Suspicious-Logic 参照)
  4. プラグインディスパッチ (render.ts:48-53):
    • atts = parseAtts(target) を取得
    • isDOMContentLoaded && !map[isRemoved] なら plugins.forEach(plugin => plugin(map, target, atts)) で即実行
    • そうでなければ alreadyRenderedMaps.push({ map, target, atts }) でキューに退避

IntersectionObserver の挙動 (render.ts:67-80)

const observer = new IntersectionObserver((entries) => {
  entries.forEach((item) => {
    if (!item.isIntersecting) return;
    try {
      renderSingleMap(item.target);
    } catch (e) {
      console.error('[Geolonia] Failed to initialize map', e);
    }
    observer.unobserve(item.target);
  });
});
  • 監視対象は .geolonia:not([data-lazy-loading="off"])
  • 初回 isIntersecting=true で render → unobserve で即座に監視解除
  • 再度 viewport に入っても再 render しない (once-only 設計)
  • maplibre 自身は display: none でも container.clientHeight が 0 になるので、container がある瞬間に lazy 解除すれば render 自体は走る (見えなくても初期化される)
  • IntersectionObserver の options 引数なしなので、threshold = 0, root = null (= viewport)

registerPlugin(plugin) (render.ts:116-121)

plugins.push(plugin);
return void 0;

単純 push。同じ plugin を複数回登録すると複数回呼ばれる (重複チェック無し)。

プラグインの実行タイミング

マップ初期化 ↔ DOMContentLoaded 実行タイミング
マップ初期化が DOMContentLoaded alreadyRenderedMaps にキュー → DOMContentLoaded ハンドラで一括実行
マップ初期化が DOMContentLoaded 初期化直後に即実行

つまり全プラグインは「DOMContentLoaded 以降」かつ「new GeoloniaMap() 完了後」に呼ばれることが保証される。

: プラグインは map.on('load') の前に呼ばれる可能性がある (GeoloniaMap constructor は同期で完了するが、load イベントはタイル取得後)。プラグインから marker 追加等するなら map.on('load', ...) でラップ必須。

DOM クエリのタイミングの注意

document.querySelectorAll('.geolonia[data-lazy-loading="off"]')renderGeoloniaMap() 実行時点の DOM スナップショット。embed.js を head で読み込む or DOMContentLoaded 前に動的に .geolonia を増やす場合、後から追加された要素は拾われない。

書き直し時はこのへんの「いつ走るか」の保証を明確化したい。

状態フラグ一覧

フラグ 場所 意味 / 遷移
isDOMContentLoaded boolean render.ts:17 (closure) 初期 false、DOMContentLoaded で true
alreadyRenderedMaps Array<{map, target, atts}> render.ts:18 DOMContentLoaded 前のマップを保持。DOMContentLoaded 時に空配列に
isRemoved unique symbol render.ts:19 map[isRemoved]true なら削除済
keyring.#isGeoloniaStyle boolean keyring.ts:4 parseAtts 内で isGeoloniaStyleCheck の結果で更新
plugins Array<EmbedPlugin> render.ts:10 (module) registerPlugin で push のみ。pop / クリアなし

エラーハンドリング

  • renderSingleMap 呼び出しは try/catch でラップ → エラーが起きても次の map に進む (render.ts:72-77, 95-101)
  • ただし console.error のみ。ユーザに見える表示は無し。handleErrorMode (util.ts:376-392) を呼ぶのは GeoloniaMap constructor 内部の super() 失敗時のみ (geolonia-map.ts:188-194)

EmbedPlugin 型 (src/types.ts:36-44)

export type EmbedPlugin<
  PluginAttributes extends { [otherKey: string]: string } = {
    [otherKey: string]: string;
  },
> = (
  map: GeoloniaMap,
  target: HTMLElement,
  atts: EmbedAttributes & PluginAttributes,
) => void;
  • ジェネリクスで属性型を拡張可能
  • 戻り値 void (非同期は async 関数を渡せるが await されない)

怪しい点 (詳細は Suspicious-Logic)

  • IntersectionObserver の once-only 解除
  • MutationObserver の parentNode 監視
  • プラグイン Queue のメモリ解放
  • DOMContentLoaded 前の早期 render に対するプラグイン遅延実行

書き直し時の指針

  • renderGeoloniaMap は wrapper 側に残す (DOM ベースの自動初期化)
  • registerPlugin も wrapper 側 (lib 側では GeoloniaMap を直接 new するため不要)
  • ただし plugins module global の所有権は議論余地
  • PMTiles プロトコル登録は lib 側 (embed-core 側) で行う(embed-core.ts の現状を踏襲)

Clone this wiki locally