-
Notifications
You must be signed in to change notification settings - Fork 7
Module Util
src/lib/util.ts (430 行)。embed 全体で使われる共通関数を集約したファイル。
| シンボル | シグネチャ |
|---|---|
GetImageCallback |
type |
isURL(str: string) |
string | false |
isGeoloniaTilesHost(url: string | URL) |
boolean |
checkPermission() |
boolean |
getLang() |
'ja' | 'en' |
isScrollable() |
boolean |
isDomElement(o) |
o is HTMLElement |
getContainer(arg) |
HTMLElement | false |
handleMarkerOptions(options, legacyOptions) |
MarkerOptions |
getStyle(style, atts) |
string |
getOptions(container, params, atts) |
MapOptions |
parseControlOption(att) |
{ enabled: boolean, position: string | undefined } |
getSessionId(digit) |
string |
parseSimpleVector(attributeValue) |
string |
handleRestrictedMode(map) |
void |
handleErrorMode(container) |
void |
sanitizeDescription(description) |
Promise<string> |
random(max: number) |
number |
loadImageCompatibility(promise, callback) |
void |
ファイル末尾の sessionId モジュール変数も (副作用):
let sessionId = '';export function isURL(str: string): string | false {
if (str.match(/^https?:\/\//)) {
return str;
} else if (str.match(/^\//) || str.match(/^\.\.?/)) {
try {
return new URL(str, location.href).href;
} catch (error) {
console.error('[Geolonia]', error);
return false;
}
}
return false;
}判定:
-
http:///https://で始まる → そのまま返す -
/で始まる、または.で始まる (相対) →new URL(str, location.href).hrefで絶対化 - それ以外 →
false
注: ^\.\.? は . または .. で始まる (= ./ または ../) を意図しているが、.foo のような単純な dot prefix も match する (regex の正確な意味)。
export function isGeoloniaTilesHost(url: string | URL): boolean {
try {
const urlObj = typeof url === 'string' ? new URL(url) : url;
return (
// NOTE: geolonia use tileserver.geolonia.com or *.tiles.geolonia.com for tile server. Other domains is NOT allowed.
urlObj.hostname === 'tileserver.geolonia.com' ||
urlObj.hostname.endsWith('.tiles.geolonia.com')
);
} catch {
return false;
}
}ホストが Geolonia 内部かどうかの whitelist:
-
tileserver.geolonia.com(完全一致) -
*.tiles.geolonia.com(.tiles.geolonia.comで終わる)
ソースコメントに NOTE 付き。新ホスト追加時はここを更新する設計。
iframe からの embed 利用許可判定。詳細は Constants の iframe 許可特例セクション。
ロジック (上から順):
-
window.self === window.parent→ iframe ではない →true -
keyring.apiKeyがある →true(キーありなら制限なし) - Codepen の origin (
cdpn.io/codepen.io) かつ referrer がhttps://codepen.ioで始まる →true - JSFiddle の origin (
fiddle.jshell.net) かつ referrer がhttps://jsfiddle.net→true - CodeSandbox の origin (
*.csb.appregex) かつ referrer がhttps://codesandbox.io→true -
window.top.location.originと一致 (= 同一 origin) →true - それ以外 →
false
ステップ 6 は cross-origin policy で window.top.location が読めない場合があり、try/catch で false 返却。
export function getLang() {
const lang =
(window.navigator.languages &&
window.navigator.languages[0] &&
window.navigator.languages[0].toLowerCase()) ||
window.navigator.language.toLowerCase();
if (lang === 'ja' || lang === 'ja-jp') {
return 'ja';
} else {
return 'en';
}
}navigator.languages[0] を優先、なければ navigator.language。'ja' / 'ja-jp' のみ 'ja'、それ以外は 'en'。
export function isScrollable() {
const body = document.body;
const html = document.documentElement;
return (
body.scrollHeight > body.clientHeight ||
html.scrollHeight > html.clientHeight
);
}ページがスクロール可能か (body または html の scrollHeight > clientHeight)。GestureHandling の条件で使う。
export function isDomElement(o): o is HTMLElement {
return typeof HTMLElement === 'object'
? o instanceof HTMLElement
: o &&
typeof o === 'object' &&
o !== null &&
o.nodeType === 1 &&
typeof o.nodeName === 'string';
}DOM2 サポートの環境では instanceof HTMLElement、そうでない (例: jsdom 古いバージョン) 場合は duck typing。
export function getContainer(
arg: HTMLElement | string | { container: HTMLElement | string },
): HTMLElement | false {
if (isDomElement(arg)) {
return arg;
} else if (typeof arg === 'string') {
const el =
(document.querySelector(arg) as HTMLElement) ||
document.getElementById(arg);
return el || false;
} else if (arg.container) {
if (isDomElement(arg.container)) {
return arg.container;
} else if (typeof arg.container === 'string') {
const el =
(document.querySelector(arg.container) as HTMLElement) ||
document.getElementById(arg.container);
return el || false;
}
}
return false;
}3 通りの入力を受ける:
- HTMLElement → そのまま返す
- string →
querySelector(string)またはgetElementById(string)で要素取得 -
{ container: HTMLElement | string }→ 同上をcontainerプロパティから
getElementById fallback: string が ID 形式 ('#mymap' ではなく 'mymap') でも動くようにする。querySelector('mymap') は要素タグとしてマッチを試みて false になるが、その後 getElementById('mymap') を試す。
export function handleMarkerOptions(
options: MarkerOptions | null | undefined | false,
legacyOptions: MarkerOptions,
) {
if (options && isDomElement(options)) {
options = {
element: options as HTMLElement,
...legacyOptions,
};
} else if (!options) {
options = legacyOptions;
}
return options;
}旧 API new Marker(htmlElement, { ... }) を新 API new Marker({ element, ... }) に変換する後方互換ラッパ。
スタイル名 / URL を CDN URL に解決する。
export function getStyle(style, atts) {
if (keyring.isGeoloniaStyleCheck(style) && !atts.key) {
throw new Error('[Geolonia] API key is required to use Geolonia styles. Please provide an API key or use an external style URL.');
}
if (!style || style === '') {
if (atts.lang === 'ja') {
return 'https://cdn.geolonia.com/style/geolonia/basic-v2/ja.json';
} else {
return 'https://cdn.geolonia.com/style/geolonia/basic-v2/en.json';
}
}
const styleUrl = isURL(style);
if (styleUrl) {
if (location.protocol === 'https:' && styleUrl.startsWith('http:')) {
console.warn(`[Geolonia] Mixed Content Warning: ... (${styleUrl}) ...`);
}
return styleUrl;
}
if (style.endsWith('.json')) {
try {
const absoluteUrl = new URL(style, location.href).href;
if (location.protocol === 'https:' && absoluteUrl.startsWith('http:')) {
console.warn(`[Geolonia] Mixed Content Warning: ... (${absoluteUrl}) ...`);
}
return absoluteUrl;
} catch (error) {
console.error('[Geolonia] Failed to resolve style URL:', style, error);
return style; // fallback
}
}
if (atts.lang === 'ja') {
return `https://cdn.geolonia.com/style/${style}/ja.json`;
} else {
return `https://cdn.geolonia.com/style/${style}/en.json`;
}
}分岐:
- Geolonia スタイルなのに key なし → throw
- 空 / null → CDN デフォルト URL (lang 別)
-
isURL(style)が truthy (= http/https/相対) → 解決済み URL を返す -
.jsonで終わる → 相対 URL とみなしてnew URL(style, location.href)で resolve -
それ以外 (論理名) →
https://cdn.geolonia.com/style/{style}/{lang}.json
Mixed Content 警告: https ページから http URL を読もうとすると console.warn (実際にはブラウザがブロック)。
MapLibre MapOptions オブジェクト構築。
export function getOptions(container, params, atts): MapOptions {
if (params.container) {
delete params.container;
}
if (params === container) {
params = {};
}
const options: MapOptions = {
style: atts.style || params.style,
container,
center: [parseFloat(atts.lng), parseFloat(atts.lat)],
bearing: parseFloat(atts.bearing),
pitch: parseFloat(atts.pitch),
zoom: parseFloat(atts.zoom),
hash: atts.hash === 'on',
localIdeographFontFamily: 'sans-serif',
attributionControl: false,
};
if (atts.minZoom !== '' && (Number(atts.minZoom) === 0 || Number(atts.minZoom))) {
options.minZoom = Number(atts.minZoom);
}
if (atts.maxZoom !== '' && Number(atts.maxZoom)) {
options.maxZoom = Number(atts.maxZoom);
}
Object.assign(options, params);
return options;
}特徴:
-
params.containerを削除 (container を上書きしない) -
params === container(HTMLElement) の場合 params を空にする —new GeoloniaMap(htmlElement)のように HTMLElement 直渡しのケースを救う -
styleは atts 優先、なければ params から (atts が parse-atts でデフォルト'geolonia/basic-v2'を持つので、実質は atts が常に勝つ) -
attributionControl: falseで maplibre 標準を無効化 (CustomAttributionControl で代替) -
localIdeographFontFamily: 'sans-serif'で CJK の sans-serif fallback -
minZoom/maxZoom条件は'0'文字列を許容する特殊ロジック (Suspicious-Logic参照) -
最後に
Object.assign(options, params)—paramsの値が options を上書き
export function parseControlOption(att) {
const normalizedAtt = att.toLowerCase();
if (['top-right', 'bottom-right', 'bottom-left', 'top-left'].includes(normalizedAtt)) {
return { enabled: true, position: normalizedAtt };
} else if (['on', 'off'].includes(normalizedAtt)) {
return { enabled: normalizedAtt === 'on', position: void 0 };
} else {
return { enabled: false, position: void 0 };
}
}戻り値 { enabled, position }。position は maplibre のデフォルト挙動を期待する場合は undefined。
let sessionId = '';
export const getSessionId = (digit) => {
if (sessionId) {
return sessionId;
} else {
const array = new Uint8Array(digit / 2);
window.crypto.getRandomValues(array);
const value = Array.from(array, (dec) =>
dec.toString(16).padStart(2, '0'),
).join('');
sessionId = value;
return value;
}
};- モジュールスコープ
sessionId変数にキャッシュ - 初回呼び出しで
crypto.getRandomValuesで hex 文字列 (digit 桁) を生成 - 2 回目以降は同じ値を返す
-
digit / 2バイト =digit桁 hex (1 バイト = 2 hex)
digit は呼び出し側 (geolonia-map.ts:121) で 40 固定。
export const parseSimpleVector = (attributeValue) => {
if (/^(https?|geolonia):\/\//.test(attributeValue)) {
return attributeValue;
} else {
return `geolonia://tiles/custom/${attributeValue}`;
}
};data-simple-vector 属性の値を URL に変換:
-
http:///https:///geolonia://で始まる → そのまま - それ以外 (= カスタムタイル ID) →
geolonia://tiles/custom/{id}でラップ
geolonia://... は geolonia-map.ts:139-146 の transformRequest で https://tileserver.geolonia.com/customtiles/{id}/tiles.json に再変換される。
export const handleRestrictedMode = (map) => {
if (!map._geolonia_restricted_mode_handled) {
map._geolonia_restricted_mode_handled = true;
const container = map.getContainer();
map.remove();
container.innerHTML = '';
container.classList.add('geolonia__restricted-mode-image-container');
}
};HTTP 402 検出時に呼ばれる。冪等性のため _geolonia_restricted_mode_handled フラグで再入防止。
container を空にしてクラスを付け、CSS 側 (docs/style.css 等) で背景画像を表示する想定。
export const handleErrorMode = (container) => {
const errorContainer = document.createElement('div');
errorContainer.classList.add('geolonia__error-container');
const div = document.createElement('div');
const h1 = document.createElement('h2');
h1.textContent = 'Geolonia Maps';
div.appendChild(h1);
div.classList.add('geolonia__error-message');
div.innerHTML +=
'<div class="geolonia__error-message-description">地図の初期化に失敗しました。管理者にお問い合わせ下さい。開発者の方は開発者ツールでエラー詳細をご確認下さい。</div>';
errorContainer.appendChild(div);
container.appendChild(errorContainer);
};maplibre.Map constructor 失敗時に呼ばれる (geolonia-map.ts:192)。
日本語固定メッセージ。多言語対応の予定はあるか不明 (Suspicious-Logic 参照)。
export const sanitizeDescription = async (description) => {
const { default: sanitizeHtml } = await import('sanitize-html');
return sanitizeHtml(description, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
'*': ['class'],
},
});
};- 動的 import で sanitize-html をロード (chunk 分割される)
- 許可タグ: default +
img - 許可属性: default + 全要素で
class
詳細は Hidden-Spec の sanitize-html 節。
export const random = (max: number): number => Math.floor(Math.random() * max);範囲 [0, max) の整数。embed コードベース内では呼び出し箇所なし (デッドコード?)。
export function loadImageCompatibility(
promise: Promise<GetResourceResponse<HTMLImageElement | ImageBitmap>>,
callback: GetImageCallback,
): void {
promise
.then((response) => {
callback(null, response.data, {
cacheControl: response.cacheControl,
expires: response.expires,
});
return;
})
.catch((error) => {
callback(error);
});
}maplibre v4 で Map.loadImage(url, callback) が loadImage(url): Promise に変更されたことへの後方互換。geolonia-map.ts:407-428 の loadImage Override で使用。
let sessionId = ''; // util.ts:337getSessionId のキャッシュ。ページ単位で 1 つ(モジュール初回ロード時に '' に初期化、初回 getSessionId で hex 値を入れる)。
-
getOptionsのparams === container比較 (HTMLElement 等価判定) -
minZoomのNumber(...) === 0 || Number(...)冗長条件 - iframe 許可リストの維持負担
-
getStyleの Mixed Content 判定 (resolve 失敗時の fallback) -
getSessionIdのグローバルキャッシュ -
handleErrorModeの日本語固定メッセージ -
randomデッドコード
- 関数群を機能別に分割 (network / DOM / parse 等)
- 副作用関数 (
handleRestrictedMode,handleErrorMode) は DOM 側 (wrapper) に - pure utility (
isURL,isGeoloniaTilesHost,parseControlOption等) は lib に -
getSessionIdのキャッシュをどう扱うかは要検討 (lib 側に持つと複数インスタンス問題) -
sanitizeDescriptionの動的 import は bundle 分割の都合 — lib 側で同様にできるか確認