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

util.ts — ユーティリティ関数群

src/lib/util.ts (430 行)。embed 全体で使われる共通関数を集約したファイル。

公開 API 一覧 (15 関数 + 2 定数)

シンボル シグネチャ
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 = '';

1. isURL(str) (util.ts:23-36)

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 の正確な意味)。

2. isGeoloniaTilesHost(url) (util.ts:38-50)

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 付き。新ホスト追加時はここを更新する設計。

3. checkPermission() (util.ts:52-120)

iframe からの embed 利用許可判定。詳細は Constants の iframe 許可特例セクション。

ロジック (上から順):

  1. window.self === window.parent → iframe ではない → true
  2. keyring.apiKey がある → true (キーありなら制限なし)
  3. Codepen の origin (cdpn.io / codepen.io) かつ referrer が https://codepen.io で始まる → true
  4. JSFiddle の origin (fiddle.jshell.net) かつ referrer が https://jsfiddle.nettrue
  5. CodeSandbox の origin (*.csb.app regex) かつ referrer が https://codesandbox.iotrue
  6. window.top.location.origin と一致 (= 同一 origin) → true
  7. それ以外 → false

ステップ 6 は cross-origin policy で window.top.location が読めない場合があり、try/catch で false 返却。

4. getLang() (util.ts:122-134)

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'

5. isScrollable() (util.ts:139-147)

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 の条件で使う。

6. isDomElement(o) (util.ts:154-162)

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。

7. getContainer(arg) (util.ts:171-193)

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') を試す。

8. handleMarkerOptions(options, legacyOptions) (util.ts:201-215)

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, ... }) に変換する後方互換ラッパ。

9. getStyle(style, atts) (util.ts:217-273)

スタイル名 / 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`;
  }
}

分岐:

  1. Geolonia スタイルなのに key なし → throw
  2. 空 / null → CDN デフォルト URL (lang 別)
  3. isURL(style) が truthy (= http/https/相対) → 解決済み URL を返す
  4. .json で終わる → 相対 URL とみなして new URL(style, location.href) で resolve
  5. それ以外 (論理名)https://cdn.geolonia.com/style/{style}/{lang}.json

Mixed Content 警告: https ページから http URL を読もうとすると console.warn (実際にはブラウザがブロック)。

10. getOptions(container, params, atts) (util.ts:277-312)

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 直渡しのケースを救う
  • styleatts 優先、なければ 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 を上書き

11. parseControlOption(att) (util.ts:319-332)

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。

12. getSessionId(digit) (util.ts:344-356)

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 固定。

13. parseSimpleVector(attributeValue) (util.ts:358-364)

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 に再変換される。

14. handleRestrictedMode(map) (util.ts:366-374)

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 等) で背景画像を表示する想定。

15. handleErrorMode(container) (util.ts:376-392)

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 参照)。

16. sanitizeDescription(description) (util.ts:394-403)

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 節。

17. random(max) (util.ts:405)

export const random = (max: number): number => Math.floor(Math.random() * max);

範囲 [0, max) の整数。embed コードベース内では呼び出し箇所なし (デッドコード?)。

18. loadImageCompatibility(promise, callback) (util.ts:411-430)

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:337

getSessionId のキャッシュ。ページ単位で 1 つ(モジュール初回ロード時に '' に初期化、初回 getSessionId で hex 値を入れる)。

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

  • getOptionsparams === container 比較 (HTMLElement 等価判定)
  • minZoomNumber(...) === 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 側で同様にできるか確認

Clone this wiki locally