-
Notifications
You must be signed in to change notification settings - Fork 7
Module GeoloniaMap
src/lib/geolonia-map.ts (429 行)。embed の中核クラス。maplibregl.Map を継承し、Geolonia 固有の認証注入・スタイル解決・SimpleStyle 統合・コントロール追加を行う。
maplibregl.Map
└── GeoloniaMap (extends)
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>>;
}type Container = HTMLElement & {
geoloniaMap: GeoloniaMap;
};HTMLElement に動的に geoloniaMap プロパティを生やすための型 hack。TypeScript は HTMLElement への任意プロパティ付与を許さないため、as Container でキャストする (geolonia-map.ts:72)。
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)
実行順序を厳密に再現する。
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) で見つからなければエラーメッセージに含める
if (container.geoloniaMap) {
return container.geoloniaMap;
}- 同じ container で 2 回目の
new GeoloniaMap()は既存インスタンスを返す -
returnで super 呼び出し前に終わるので、コンストラクタとしては本来不正だが現コードは動作している (V8 / JS engine がreturn値を採用) - 詳細は
Hidden-Specの "container.geoloniaMap シングルトン" 節
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.');
}- 警告のみ、処理は続行
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参照
const content = container.innerHTML.trim();
container.innerHTML = '';- container の元 HTML を popup 用に保存
- 直後に container を空に (maplibre が自前で canvas を入れるため)
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
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 参照。
詳細は URL-Rewrite-Spec。
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)
const map = this;
this.geoloniaSourcesUrl = sourcesUrl;
this.__styleExtensionLoadRequired = true;-
__styleExtensionLoadRequired = trueで次の styledata イベントで拡張ロードを発火
順序が重要: 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);
}特殊: GeoloniaControl は enabled を見ず常に追加される (atts.geoloniaControl === 'off' でも position: undefined でデフォルト位置に追加される)。これは意図的かバグか不明 (Suspicious-Logic 参照)。
CustomAttributionControl は data-* 属性で制御不可、常に bottom-right。
map.on('load', (event) => {...}) で登録される処理:
-
Loader 削除 (
atts.loader !== 'off'):try { container.removeChild(loading); } catch { // Nothing to do. }
try/catch は loader 削除失敗時の安全弁。なぜ失敗するかは不明 (
Suspicious-Logic参照) -
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) -
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'クラスを付与
-
map.on('styledata', async (event) => {
if (!this.__styleExtensionLoadRequired) return;
this.__styleExtensionLoadRequired = false;
// ... 拡張ロード ...
});__styleExtensionLoadRequired フラグで再入防止。一度処理したら false にし、setStyle() で再度 true に戻すことで再ロード。
処理内容:
-
SimpleStyleVector 追加 (
atts.simpleVector真値):const simpleVectorAttributeValue = parseSimpleVector(atts.simpleVector); new SimpleStyleVector(simpleVectorAttributeValue).addTo(map);
-
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-lngとdata-latが両方未指定の時のみ fitBounds 自動発火。 -
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参照。
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
container.geoloniaMap = map;
return map;constructor の最終行。次回 new GeoloniaMap(sameContainer) でこれが返される。
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;
}-
styleがnullのときは parseAtts も getStyle もスキップ (maplibre 仕様:nullで style を完全削除) -
string引数のときは getStyle で URL 解決して maplibre に渡す -
__styleExtensionLoadRequired = trueで styledata 再ロード強制 -
super.setStyle.call(this, ...)形式 (なぜ call 経由かは不明 — おそらく maplibre のsetStyle内thisバインド対策)
コメント (geolonia-map.ts:381-383) より:
It can't access
thisbecausesetStyle()will be called withsuper(). So, we need to runparseAtts()again(?)
つまり constructor 内で super() 経由で setStyle が呼ばれた際、setStyle 内で this が未初期化なので parseAtts を再実行する、という設計。
remove(): void {
const container = this.getContainer();
super.remove.call(this);
delete (container as HTMLElement & { geoloniaMap: GeoloniaMap }).geoloniaMap;
}container プロパティを clear して重複防止キャッシュを解放。
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) |
- 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 側で持たせるか議論