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

GeoloniaMap クラス

src/lib/geolonia-map.ts (429 行)。embed の中核クラス。maplibregl.Map を継承し、Geolonia 固有の認証注入・スタイル解決・SimpleStyle 統合・コントロール追加を行う。

クラス階層

maplibregl.Map
  └── GeoloniaMap (extends)

公開 API (TS シグネチャ)

export type GeoloniaMapOptions = MapOptions & { interactive?: boolean };

export default class GeoloniaMap extends maplibregl.Map {
  private geoloniaSourcesUrl: URL;
  private __styleExtensionLoadRequired: boolean;

  constructor(params: string | GeoloniaMapOptions);

  // Override
  setStyle(
    style: string | StyleSpecification,
    options?: StyleSwapOptions & StyleOptions,
  ): this;

  // Override
  remove(): void;

  // Override (maplibre v4 callback compat)
  loadImage(url: string, callback: GetImageCallback): void;
  loadImage(url: string): Promise<GetResourceResponse<HTMLImageElement | ImageBitmap>>;
}

ファイル内ヘルパー型・関数

Container (geolonia-map.ts:40-42)

type Container = HTMLElement & {
  geoloniaMap: GeoloniaMap;
};

HTMLElement に動的に geoloniaMap プロパティを生やすための型 hack。TypeScript は HTMLElement への任意プロパティ付与を許さないため、as Container でキャストする (geolonia-map.ts:72)。

isCssSelector(string) (geolonia-map.ts:44-60)

const isCssSelector = (string): Element | null | false => {
  if (/^https?:\/\//.test(string)) return false;
  if (/^\//.test(string)) return false;
  if (/^\.\//.test(string)) return false;
  if (/^\.\.\//.test(string)) return false;
  try {
    return document.querySelector(string);
  } catch {
    return false;
  }
};
  • URL / 相対パスはまず弾く
  • それ以外を querySelector で試行
  • マッチした Element / null (見つからない) / false (URL 等) を返す
  • 用途: data-geojson 属性が URL なのか CSS selector なのか判定 (geolonia-map.ts:305)

Constructor の完全フロー (geolonia-map.ts:71-369)

実行順序を厳密に再現する。

1. Container 解決 (geolonia-map.ts:72-84)

const container = getContainer(params) as Container | false;
if (!container) {
  if (typeof params === 'string') {
    throw new Error(`[Geolonia] No HTML elements found matching \`${params}\`. Please ensure the map container element exists.`);
  } else {
    throw new Error('[Geolonia] No HTML elements found. Please ensure the map container element exists.');
  }
}
  • getContainer の挙動は util.md 参照
  • 引数が string (CSS selector) で見つからなければエラーメッセージに含める

2. 重複防止 (geolonia-map.ts:86-88)

if (container.geoloniaMap) {
  return container.geoloniaMap;
}
  • 同じ container で 2 回目の new GeoloniaMap()既存インスタンスを返す
  • return で super 呼び出し前に終わるので、コンストラクタとしては本来不正だが現コードは動作している (V8 / JS engine が return 値を採用)
  • 詳細は Hidden-Spec の "container.geoloniaMap シングルトン" 節

3. height 警告 (geolonia-map.ts:90-95)

if (container.clientHeight === 0) {
  console.warn('[Geolonia] Embed API failed to render the map because the container has no height. Please set the CSS property `height` to the container.');
}
  • 警告のみ、処理は続行

4. atts と options 構築 (geolonia-map.ts:97-100)

const atts = parseAtts(container, {
  interactive: typeof params === 'object' ? params.interactive : true,
});
const options = getOptions(container, params, atts);
  • params が string (CSS selector) なら interactive = true
  • params が object なら params.interactive をそのまま渡す
  • 詳細は parse-atts.md / util.md 参照

5. innerHTML 退避 (geolonia-map.ts:102-104)

const content = container.innerHTML.trim();
container.innerHTML = '';
  • container の元 HTML を popup 用に保存
  • 直後に container を空に (maplibre が自前で canvas を入れるため)

6. Loader 追加 (geolonia-map.ts:106-119)

atts.loader !== 'off' の場合のみ:

loading = document.createElement('div');
loading.addEventListener('touchmove', (e) => {
  if (e.touches && e.touches.length > 1) {
    e.preventDefault();
  }
});
loading.className = 'loading-geolonia-map';
loading.innerHTML = `<div class="lds-grid"><div></div><div></div><div></div>
    <div></div><div></div><div></div><div></div><div></div><div></div></div>`;
container.appendChild(loading);
  • touchmove で 2 本指 (pinch) を防止 (e.preventDefault())
  • loading-geolonia-map クラスの spinner

7. sessionId & sourcesUrl 構築 (geolonia-map.ts:121-124)

const sessionId = getSessionId(40);
const sourcesUrl = new URL(`${atts.apiUrl}/sources`);
sourcesUrl.searchParams.set('key', atts.key);
sourcesUrl.searchParams.set('sessionId', sessionId);

詳細は URL-Rewrite-Spec 参照。

8. transformRequest override (geolonia-map.ts:127-186)

詳細は URL-Rewrite-Spec

9. super() 呼び出し + エラーハンドリング (geolonia-map.ts:188-194)

try {
  super(options);
} catch (error) {
  handleErrorMode(container);
  throw error;
}
  • maplibregl.Map constructor 失敗時は handleErrorMode で error message div を挿入してから再 throw
  • handleErrorMode の DOM: <div class="geolonia__error-container"><div class="geolonia__error-message"><h2>Geolonia Maps</h2><div class="geolonia__error-message-description">地図の初期化に失敗しました...</div></div></div> (util.ts:376-392)

10. インスタンスメンバ設定 (geolonia-map.ts:196-198)

const map = this;
this.geoloniaSourcesUrl = sourcesUrl;
this.__styleExtensionLoadRequired = true;
  • __styleExtensionLoadRequired = true で次の styledata イベントで拡張ロードを発火

11. コントロール追加 (geolonia-map.ts:200-237)

順序が重要: GeoloniaControl は最初に追加 (default position は bottom-left)。map.addControl は同じ position に対し後勝ちで重ねるので、GeoloniaControl をまず置いてから他を載せる。

const { position: geoloniaControlPosition } = parseControlOption(atts.geoloniaControl);
map.addControl(new GeoloniaControl(), geoloniaControlPosition);

map.addControl(new CustomAttributionControl(), 'bottom-right');

if (parseControlOption(atts.fullscreenControl).enabled) {
  map.addControl(new FullscreenControl(), position);
}
if (parseControlOption(atts.navigationControl).enabled) {
  map.addControl(new NavigationControl(), position);
}
if (parseControlOption(atts.geolocateControl).enabled) {
  map.addControl(new GeolocateControl({}), position);
}
if (parseControlOption(atts.scaleControl).enabled) {
  map.addControl(new ScaleControl({}), position);
}

特殊: GeoloniaControlenabled を見ず常に追加される (atts.geoloniaControl === 'off' でも position: undefined でデフォルト位置に追加される)。これは意図的かバグか不明 (Suspicious-Logic 参照)。

CustomAttributionControl は data-* 属性で制御不可、常に bottom-right。

12. load イベントハンドラ (geolonia-map.ts:239-289)

map.on('load', (event) => {...}) で登録される処理:

  1. Loader 削除 (atts.loader !== 'off'):

    try {
      container.removeChild(loading);
    } catch {
      // Nothing to do.
    }

    try/catch は loader 削除失敗時の安全弁。なぜ失敗するかは不明 (Suspicious-Logic 参照)

  2. GestureHandling 追加 (atts.gestureHandling !== 'off' && isScrollable()):

    new GestureHandling({ lang: atts.lang }).addTo(map);

    isScrollable()document.body.scrollHeight > body.clientHeight || html.scrollHeight > html.clientHeight (util.ts:139-147)

  3. Marker / Popup 追加 (atts.lat && atts.lng && atts.marker === 'on'):

    • content (退避済み innerHTML) があれば popup 付き marker
    • atts.customMarker 指定 → 既存 DOM 要素を使う
    • 無ければデフォルト GeoloniaMarker({ color: atts.markerColor })
    • atts.openPopup === 'on'marker.togglePopup() で初期表示
    • marker 要素に 'geolonia-clickable-marker' クラスを付与

13. styledata イベントハンドラ (geolonia-map.ts:291-357)

map.on('styledata', async (event) => {
  if (!this.__styleExtensionLoadRequired) return;
  this.__styleExtensionLoadRequired = false;
  // ... 拡張ロード ...
});

__styleExtensionLoadRequired フラグで再入防止。一度処理したら false にし、setStyle() で再度 true に戻すことで再ロード。

処理内容:

  1. SimpleStyleVector 追加 (atts.simpleVector 真値):

    const simpleVectorAttributeValue = parseSimpleVector(atts.simpleVector);
    new SimpleStyleVector(simpleVectorAttributeValue).addTo(map);
  2. GeoJSON / SimpleStyle 追加 (atts.geojson 真値):

    const el = isCssSelector(atts.geojson);
    let json;
    if (el) {
      json = JSON.parse(el.textContent);  // インライン
    } else {
      json = atts.geojson;  // URL (内部で fetch)
    }
    const ss = new SimpleStyle(json, {
      cluster: atts.cluster === 'on',
      clusterColor: atts.clusterColor,
    });
    ss.addTo(map);
    if (!container.dataset || (!container.dataset.lng && !container.dataset.lat)) {
      ss.fitBounds();
    }

    data-lngdata-lat が両方未指定の時のみ fitBounds 自動発火。

  3. 3D layer 切替 (atts['3d'] 真値):

    const style = map.getStyle();
    style.layers.forEach((layer) => {
      if (atts['3d'] === 'on' && layer.metadata?.['visible-on-3d'] === true) {
        map.setLayoutProperty(layer.id, 'visibility', 'visible');
      } else if (atts['3d'] === 'off' && layer.metadata?.['visible-on-3d'] === true) {
        map.setLayoutProperty(layer.id, 'visibility', 'none');
      } else if (atts['3d'] === 'on' && layer.metadata?.['hide-on-3d'] === true) {
        map.setLayoutProperty(layer.id, 'visibility', 'none');
      } else if (atts['3d'] === 'off' && layer.metadata?.['hide-on-3d'] === true) {
        map.setLayoutProperty(layer.id, 'visibility', 'visible');
      }
    });

    data-3d の値が 'on' / 'off' のとき、style 内の全レイヤを走査し metadata の visible-on-3d / hide-on-3d を見て visibility 切り替え。詳細は Hidden-Spec 参照。

14. error イベントハンドラ (geolonia-map.ts:359-364)

map.on('error', async (error) => {
  if (error.error && error.error.status === 402) {
    handleRestrictedMode(map);
  }
});

HTTP 402 (Payment Required) → handleRestrictedMode(map) (util.ts:366-374):

  • map.remove()
  • container.innerHTML = ''
  • container.classList.add('geolonia__restricted-mode-image-container')
  • 同じ map に対して 2 回呼ばれないよう map._geolonia_restricted_mode_handled フラグで guard

15. 重複防止キャッシュ (geolonia-map.ts:366-368)

container.geoloniaMap = map;
return map;

constructor の最終行。次回 new GeoloniaMap(sameContainer) でこれが返される。

setStyle(style, options) Override (geolonia-map.ts:376-398)

setStyle(style: string | StyleSpecification, options: StyleSwapOptions & StyleOptions = {}): this {
  if (style !== null) {
    const atts = parseAtts(this.getContainer());

    if (typeof style === 'string') {
      style = getStyle(style, atts);
    }
  }

  this.__styleExtensionLoadRequired = true;
  super.setStyle.call(this, style, options);
  return this;
}
  • stylenull のときは parseAtts も getStyle もスキップ (maplibre 仕様: null で style を完全削除)
  • string 引数のときは getStyle で URL 解決して maplibre に渡す
  • __styleExtensionLoadRequired = true で styledata 再ロード強制
  • super.setStyle.call(this, ...) 形式 (なぜ call 経由かは不明 — おそらく maplibre の setStylethis バインド対策)

コメント (geolonia-map.ts:381-383) より:

It can't access this because setStyle() will be called with super(). So, we need to run parseAtts() again(?)

つまり constructor 内で super() 経由で setStyle が呼ばれた際、setStyle 内で this が未初期化なので parseAtts を再実行する、という設計。

remove() Override (geolonia-map.ts:400-405)

remove(): void {
  const container = this.getContainer();
  super.remove.call(this);
  delete (container as HTMLElement & { geoloniaMap: GeoloniaMap }).geoloniaMap;
}

container プロパティを clear して重複防止キャッシュを解放。

loadImage() Override (geolonia-map.ts:407-428)

maplibre v4.0.0 で loadImage(url, callback)loadImage(url): Promise に変更された後方互換ラッパ。

loadImage(url: string, callback?: GetImageCallback): Promise<...> | void {
  const promise = super.loadImage(url);
  if (callback) {
    loadImageCompatibility(promise, callback);
  } else {
    return promise;
  }
}

loadImageCompatibility (util.ts:411-430) は promise を (error, result, expiry) 形式の callback に変換。

状態フラグ一覧

フラグ 場所 意味
__styleExtensionLoadRequired boolean instance styledata 拡張 (SimpleStyle/3D) のロード必要フラグ。constructor で true、styledata 処理直前に false、setStyle で true
geoloniaSourcesUrl URL instance sources エンドポイント URL (transformRequest 内で参照用に保持)
container.geoloniaMap GeoloniaMap container プロパティ 重複生成防止
map._geolonia_restricted_mode_handled boolean instance (動的プロパティ) 402 ハンドラの再入防止 (util.ts:367)

構造的に怪しい点 (詳細は Suspicious-Logic)

  • container.geoloniaMap シングルトンのキャッシュ
  • GeoloniaControl が enabled を見ない
  • loader 削除の try/catch
  • minZoom の Number(...) === 0 || Number(...) 条件
  • dev hostname 置換が tileserver のみ (sprite は別ロジック)
  • localIdeographFontFamily hard-coded
  • setStyle 内の super.setStyle.call(this, ...)

書き直し時の指針

  • maps-core (lib) 側に置く。MapLibre Map のサブクラスとして残す
  • transformRequest は静的関数として切り出してテスタブルに
  • 3D toggle ロジックは独立メソッドへ
  • container.geoloniaMap キャッシュは lib 側で保持するか wrapper 側で持たせるか議論

Clone this wiki locally