MapLibreの使い方入門|コピペで動くサンプルコードで学ぶ地図表示とGeoJSON操作

javascript
記事内に広告が含まれています。

「MapLibreの使い方を調べているけど、情報が少なくてよく分からない…」「Mapboxとの違いは?本当に無料で使えるの?」そんな悩みを感じていませんか?

MapLibreは、Mapbox GL JSから派生したOSSの地図ライブラリとして注目されていますが、日本語の情報が少なく、公式ドキュメントもやや分かりづらいため、結局どうやって使えばいいのか分からないという方が多いのが現状です。特に、導入方法・地図表示・マーカー設置・3D表現など、実務で必要なポイントを体系的に解説している記事は多くありません。

そこで本記事では、MapLibre初心者の方でも迷わないように、最小構成のコードから実務レベルの実装までを分かりやすく解説します。MapboxやLeafletとの違い、商用利用のポイントも含めて、これ1本で全体像がつかめる内容になっています。

この記事を読んでわかること

  • MapLibre GL JSの特徴とMapbox・Leafletとの違い
  • CDNやnpmを使った導入方法と地図の基本表示
  • center・zoom・pitchなど初期設定の考え方
  • マーカー・ポップアップ・イベント処理の実装方法
  • GeoJSONを使った複数地点表示やデータ描画
  • LineStringやPolygonを使ったルート・エリア表現
  • 3D建物や地形(Terrain)の表示方法
  • 商用利用・ライセンス・コストの考え方

「とりあえず動かしたい」という方から、「実務で使いたい」という方まで対応しているので、ぜひ最後までチェックしてみてください。

MapLibreとは?MapLibre GL JSの特徴とMapbox・Leafletとの違い

MapLibre GL JSは、WebGLを使ってブラウザ上に高性能な地図を描画するオープンソースライブラリです。Google Mapsのような商用サービスに頼らず、自社プロダクトに地図機能を組み込みたいエンジニアにとって、現在最も有力な選択肢のひとつとなっています。

MapLibreの特徴とMapbox GL JSからフォークされた背景

MapLibre GL JSは、Mapbox GL JS v1系をベースにフォークされたライブラリです。

Mapboxは地図ライブラリ界隈で長年リードしてきた企業ですが、2020年12月にMapbox GL JS v2のライセンスを独自のビジネスソースライセンス(BSL)に変更しました。これにより、v2以降はOSSとして自由に使えなくなり、Mapboxのサービスへの依存が実質的に必須となりました。

この変更に反発したコミュニティが、ライセンス変更前の最後のOSSバージョン(v1.13)をベースに独立したプロジェクトとしてフォークしたのがMapLibre GL JSの始まりです。現在はLinux Foundation傘下のMapLibre Organizationによってメンテナンスされており、企業・個人問わず多くのコントリビューターが開発に参加しています。

MapLibreの主な特徴を以下にまとめます。

特徴内容
WebGLレンダリングGPUを活用した高速・滑らかな地図描画
ベクタータイル対応スタイルをCSS感覚で自由にカスタマイズ可能
完全OSSBSDライセンスで商用利用・改変・再配布が自由
Mapbox互換Mapbox GL JS v1向けのコードが概ねそのまま動作
積極的なコミュニティ開発v3系ではWebGPU対応なども進行中

特に「Mapbox GL JSのコードをほぼそのまま移植できる」点は、既存資産の再利用という観点で大きなメリットです。

商用利用は完全無料?BSDライセンスの条件とコストメリット

結論から言うと、MapLibre GL JS自体は商用プロダクトでも無料で利用できます

MapLibreはBSD-3-Clauseライセンスを採用しています。このライセンスで求められる条件は非常にシンプルで、主に以下の3点です。

  • ソースコードを再配布する場合、著作権表示を保持すること
  • バイナリ形式で再配布する場合、ドキュメント等に著作権表示を含めること
  • 開発者の名称をプロモーションに無断で使用しないこと

つまり、「MapLibreを使ったサービスで収益を得ること」自体には一切制限がありません。SaaS、業務システム、モバイルアプリのWebView内など、あらゆる商用ユースケースで無料で利用できます。

ただし、注意点が1つあります。MapLibreはあくまで地図を描画するレンダリングエンジンであり、地図の元データ(タイルデータ)は別途用意する必要があります。タイルデータの選択肢としては以下があります。

タイルソース費用備考
OpenStreetMap(Raster Tiles)無料利用規約の確認が必要
OpenMapTiles無料〜有料セルフホストも可能
MAPTILER無料枠あり商用向け有料プランも充実
Mapbox Tiles無料枠あり一定量超で課金
国土地理院タイル無料日本国内の用途に最適

コスト最小で運用したい場合、レンダリングにMapLibre + タイルデータに国土地理院(日本国内)またはOpenStreetMapを組み合わせる構成が鉄板です。

Leafletとの違いと「maplibre vs leaflet」どちらを選ぶべきか

Leafletも非常に人気の高いOSS地図ライブラリですが、設計思想とレンダリング方式が根本的に異なります。どちらを選ぶべきかは、要件によって明確に判断できます。

レンダリング方式の違い

  • Leaflet:DOM(HTML要素)ベースのレンダリング。タイル画像をHTMLの<img>タグとして並べて地図を構成します。
  • MapLibre:WebGLベースのレンダリング。GPUで地図全体を1枚のCanvasとして描画します。

この違いが、以下のような性能差・機能差に直結します。

比較項目MapLibre GL JSLeaflet
レンダリングWebGL(GPU)DOM/Canvas
パフォーマンス大量データでも高速データ量が増えると重くなりやすい
3D表示✅ネイティブ対応❌プラグイン必要
ベクタータイル✅ネイティブ対応⚠️プラグイン必要
スタイルのカスタマイズJSONスタイルシートで高度に制御可能CSSベースでシンプル
学習コストやや高め低い
ライブラリサイズ大きめ(〜約700KB gzip前)軽量(〜約140KB gzip前)
IE対応など古いブラウザ❌WebGL非対応環境では動作不可✅比較的広い対応

どちらを選ぶべきか?

以下の判断基準を参考にしてください。

MapLibreを選ぶべきケース

  • 3D地図や地形表現、建物の立体表示が必要
  • 大量のGeoJSONや店舗データ(数千件以上)をスムーズに表示したい
  • 地図のデザインをブランドに合わせて細かくカスタマイズしたい
  • ルート表示やアニメーションを伴うリッチな地図UXを作りたい

Leafletを選ぶべきケース

  • シンプルなマーカー表示や地図埋め込みで十分
  • 学習コストを抑えて素早く実装したい
  • 古い社内システムや幅広いブラウザへの対応が必要
  • ライブラリの依存を最小限にしたい軽量なプロジェクト

モダンな商用Webアプリケーションにおいて、地図が中心機能となる場合はMapLibreが有力です。一方で、「地図はあくまでサブ機能」「とにかく手軽に実装したい」という場合はLeafletのシンプルさが魅力的です。

MapLibre GL JSの導入手順と地図表示の基本

MapLibre GL JSを実際のプロジェクトに導入する方法は大きく2つあります。CDN経由で即座に試す方法と、npmを使ってモダンなJS環境に組み込む方法です。まずはそれぞれの手順を押さえ、地図の初期表示に必要な基本設定まで一気に理解しましょう。

CDN経由で今すぐ動かす!最小構成のHTMLサンプルコード

最も手軽に始めるなら、CDNを使う方法が最速です。 Node.jsやビルドツールの準備が不要で、HTMLファイル1枚だけで地図を表示できます。プロトタイプの検証や社内ツールの素早い立ち上げに最適です。

以下が、コピペしてすぐに動く最小構成のサンプルコードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>MapLibre GL JS - 最小構成サンプル</title>

  <!-- ① MapLibre GL JSのCSSを読み込む -->
  <link
    rel="stylesheet"
    href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css"
  />

  <style>
    /* ② 地図コンテナを画面全体に広げる */
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
    }
    #map {
      width: 100%;
      height: 100%;
    }
  </style>
</head>
<body>

  <!-- ③ 地図を描画するコンテナ要素 -->
  <div id="map"></div>

  <!-- ④ MapLibre GL JSのJavaScriptを読み込む -->
  <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>

  <script>
    // ⑤ Mapオブジェクトを生成して地図を初期化する
    const map = new maplibregl.Map({
      container: 'map', // 描画先のHTML要素のid
      style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json', // タイルスタイル
      center: [139.6917, 35.6895], // 初期表示の中心座標 [経度, 緯度](東京)
      zoom: 12 // 初期ズームレベル
    });
  </script>

</body>
</html>

実際の表示

See the Pen maplibre-usage-01 by watashi-xyz (@watashi-xyz) on CodePen.

各ポイントの解説

  • CSSの読み込みを忘れないことが、MapLibreを導入する際の最初のつまずきポイントです。ズームボタンやコピーライト表示などのUIコンポーネントのスタイルはこのCSSに含まれており、省略するとレイアウトが崩れます。
  • コンテナ要素に高さを指定することも必須です。#map要素はデフォルトでは高さ0になるため、CSSで明示的にサイズを与えないと地図が表示されません。height: 100vhでも構いません。
  • <div id="map">は地図を描画するWebGLのCanvasが挿入される親要素です。MapLibreはこの要素の中に<canvas>を自動生成して描画を行います。
  • new maplibregl.Map({...})が地図インスタンスを生成するコアの処理です。オプションオブジェクトの各プロパティについては後述します。

styleプロパティについて

上記サンプルではtile.openstreetmap.jpのスタイルJSONを使用しています。これはOpenStreetMapベースの無料タイルで、日本国内の利用に適しています。本番環境では利用規約を確認のうえ、MapTilerやProtomapsなど商用利用に適したタイルプロバイダーの検討を推奨します。

npm/yarnでのインストールとモダンJS環境(ES Modules)での読み込み

React、Vue、SvelteなどのフレームワークやVite・webpackを使うプロジェクトでは、npmでインストールするのが標準的な方法です。

インストール

# npm
npm install maplibre-gl

# yarn
yarn add maplibre-gl

# pnpm
pnpm add maplibre-gl

ES Modulesでの読み込み(Vanilla JS / Vite環境)

// main.js
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; // CSSのインポートも忘れずに

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',
  center: [139.6917, 35.6895],
  zoom: 12
});

Reactコンポーネントでの実装例

// MapComponent.jsx
import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

export default function MapComponent() {
  const mapContainer = useRef(null); // DOMへの参照
  const map = useRef(null);          // Mapインスタンスの保持用

  useEffect(() => {
    // すでに初期化済みなら何もしない(Strict Modeの二重実行対策)
    if (map.current) return;

    map.current = new maplibregl.Map({
      container: mapContainer.current,
      style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',
      center: [139.6917, 35.6895],
      zoom: 12
    });

    // コンポーネントのアンマウント時にMapインスタンスを破棄する
    return () => {
      map.current?.remove();
      map.current = null;
    };
  }, []); // 空配列で初回マウント時のみ実行

  return (
    <div
      ref={mapContainer}
      style={{ width: '100%', height: '100vh' }}
    />
  );
}

Reactで実装する際の重要ポイントが2つあります。

  • 1つ目はuseRefでMapインスタンスを保持することです。useStateを使うとMapオブジェクトの変化のたびに再レンダリングが走り、地図が再初期化されてしまいます。Mapインスタンスはコンポーネントのライフサイクル外で保持すべきものなので、useRefが適切です。
  • 2つ目はクリーンアップ関数でmap.remove()を呼ぶことです。コンポーネントがアンマウントされた際にMapインスタンスが残留すると、WebGLコンテキストのリークやメモリ問題につながります。useEffectのクリーンアップ関数内で確実に破棄してください。

Vue 3(Composition API)での実装例

<template>
  <div ref="mapContainer" style="width: 100%; height: 100vh;" />
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const mapContainer = ref(null);
let map = null;

onMounted(() => {
  map = new maplibregl.Map({
    container: mapContainer.value,
    style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',
    center: [139.6917, 35.6895],
    zoom: 12
  });
});

onUnmounted(() => {
  map?.remove();
});
</script>

地図の初期表示設定:center(経緯度)、zoom(拡大率)、pitch(傾き)

new maplibregl.Map({...})に渡す初期化オプションを理解することで、地図の見た目を自在にコントロールできます。よく使うオプションを体系的に整理します。

主要な初期化オプション一覧

オプション説明
containerstringHTMLElement
stylestringobject
center[number, number]初期中心座標。[経度, 緯度]の順序に注意(緯度・経度の逆)
zoomnumber初期ズームレベル。0(全世界)〜22(建物レベル)が目安
pitchnumber地図の傾き(度)。0が真上から、最大85まで指定可能
bearingnumber地図の回転角(度)。0が北向き、正の値で時計回りに回転
minZoomnumberユーザーが縮小できる最小ズームレベル
maxZoomnumberユーザーが拡大できる最大ズームレベル
maxBoundsLngLatBoundsLike地図のパン(移動)を制限する範囲
attributionControlboolean帰属表示(コピーライト)の表示/非表示
hashbooleanURLのハッシュに現在の座標・ズームを同期する

以下は、これらのオプションをまとめて活用したサンプルです。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 15,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});

実際の表示

See the Pen maplibre-usage-02 by watashi-xyz (@watashi-xyz) on CodePen.

特に注意すべきポイント:centerの座標の順序

MapLibreでは座標を[経度(longitude), 緯度(latitude)]の順で指定します。これはGoogle MapsやLeafletが(緯度, 経度)の順を採用しているのとは逆順です。GeoJSONの仕様に準拠したものですが、乗り換え時に混乱しやすいため注意してください。

pitchbearingで立体的な地図表現を実現:

pitch(傾き)とbearing(方位)を組み合わせると、以下のような表現が可能になります。

// 初期表示後にアニメーションで視点を変更することも可能
map.on('load', () => {
  map.easeTo({
    pitch: 60,    // 60度に傾ける
    bearing: 30,  // 30度回転
    duration: 2000 // 2秒かけてアニメーション
  });
});

easeTo()はアニメーションを伴って視点を変更するメソッドです。ユーザーが特定の場所をクリックした際に視点をスムーズに移動させるUXに活用できます。同様のメソッドとしてflyTo()(より劇的なズームイン・アウトアニメーション)やjumpTo()(即座に移動、アニメーションなし)もあります。

実際の表示

See the Pen maplibre-usage-03 by watashi-xyz (@watashi-xyz) on CodePen.

MapLibreで実装する基本操作(マーカー・ポップアップ・イベント処理)

地図を表示できたら、次のステップはマーカーの設置・ポップアップの表示・ユーザー操作への応答です。これらは地図機能の実装で最も頻繁に求められる要素であり、実務でも即座に活用できます。順を追って実装方法を解説します。

マーカー(Marker)とカスタムアイコンの表示方法

MapLibreでマーカーを表示するにはmaplibregl.Markerクラスを使います。 デフォルトでは青いピンアイコンが表示されますが、カスタムHTMLを使って任意のアイコンに差し替えることも可能です。

基本的なマーカーの追加:

// 地図の読み込み完了後にマーカーを追加するのが確実
map.on('load', () => {

  // デフォルトマーカー(青いピン)を東京駅に設置
  const marker = new maplibregl.Marker()
    .setLngLat([139.7004, 35.6580]) // [経度, 緯度]
    .addTo(map); // どのmapインスタンスに追加するかを指定

});

マーカーの色・サイズをオプションで変更:

const marker = new maplibregl.Marker({
  color: '#E74C3C',  // マーカーの色(デフォルトは青)
  scale: 1.2         // サイズ倍率(デフォルトは1)
})
  .setLngLat([139.7671, 35.6812])
  .addTo(map);

実際の表示

See the Pen maplibre-usage-04 by watashi-xyz (@watashi-xyz) on CodePen.

カスタムHTMLアイコンを使ったマーカー:

実務では、デザインに合ったカスタムアイコンを使いたい場面がほとんどです。elementオプションに任意のDOM要素を渡すことで、HTMLとCSSでマーカーを自由にデザインできます。

<style>
  .custom-marker {
    width: 40px;
    height: 40px;
    background-color: #2ECC71;
    border: 3px solid #fff;
    border-radius: 50%;          /* 円形 */
    box-shadow: 0 2px 6px rgba(0,0,0,0.3);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;             /* 絵文字アイコン用 */
  }
</style>

<script>
  // ① カスタムアイコン用のDOM要素を生成
  const el = document.createElement('div');
  el.className = 'custom-marker';
  el.textContent = '🏪'; // 絵文字をアイコンとして利用

  // ② element オプションにDOM要素を渡す
  const customMarker = new maplibregl.Marker({ element: el })
    .setLngLat([139.7030, 35.6580])
    .addTo(map);
</script>

実際の表示

See the Pen maplibre-usage-05 by watashi-xyz (@watashi-xyz) on CodePen.

マーカーのアンカーポイント(位置合わせ)を調整する:

カスタムアイコンを使う場合、アイコンのどの部分を座標に合わせるかをanchorオプションで指定できます。

const el = document.createElement('div');
el.className = 'custom-marker';

new maplibregl.Marker({
  element: el,
  anchor: 'bottom', // 'center'(デフォルト) | 'top' | 'bottom' | 'left' | 'right'
})
  .setLngLat([139.7671, 35.6812])
  .addTo(map);

ピン型アイコンであればanchor: 'bottom'を指定することで、アイコンの先端が座標に正確に合います。

マーカーをドラッグ可能にする:

const draggableMarker = new maplibregl.Marker({ draggable: true })
  .setLngLat([139.7671, 35.6812])
  .addTo(map);

// ドラッグ終了時に新しい座標を取得
draggableMarker.on('dragend', () => {
  const lngLat = draggableMarker.getLngLat();
  console.log(`新しい座標: 経度 ${lngLat.lng}, 緯度 ${lngLat.lat}`);
});

実際の表示

See the Pen maplibre-usage-06 by watashi-xyz (@watashi-xyz) on CodePen.

ポップアップの表示・クリックイベント・ホバーイベントの実装

ポップアップはmaplibregl.Popupクラスで実装します。 マーカーに紐付ける方法と、地図上の任意の座標に直接表示する方法の2パターンがあります。

マーカーにポップアップを紐付ける(最もシンプルな方法):

// Popupインスタンスを作成
const popup = new maplibregl.Popup({
  offset: 25,        // マーカーからのオフセット距離(px)
  closeButton: true, // 閉じるボタンの表示(デフォルト: true)
  closeOnClick: true // 地図クリックで閉じる(デフォルト: true)
})
  .setHTML(`
    <div style="padding: 4px 8px;">
      <h3 style="margin: 0 0 4px; font-size: 14px;">東京駅</h3>
      <p style="margin: 0; font-size: 12px; color: #666;">
        JR各線・地下鉄が乗り入れる東京の玄関口
      </p>
    </div>
  `);

// MarkerにPopupを紐付ける(マーカークリックで自動的に開く)
new maplibregl.Marker({ color: '#3498DB' })
  .setLngLat([139.7671, 35.6812])
  .setPopup(popup) // ここでPopupを紐付ける
  .addTo(map);

実際の表示

See the Pen maplibre-usage-07 by watashi-xyz (@watashi-xyz) on CodePen.

GeoJSONレイヤーのフィーチャーをクリックしてポップアップを表示:

実務で最もよく使うパターンです。地図上に描画したポイントやポリゴンをクリックした際に、そのデータに紐づく情報をポップアップで表示します。

map.on('load', () => {

  // GeoJSONソースを追加
  map.addSource('shops', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: { type: 'Point', coordinates: [139.7671, 35.6812] },
          properties: { name: '東京駅店', hours: '10:00〜21:00' }
        },
        {
          type: 'Feature',
          geometry: { type: 'Point', coordinates: [139.7003, 35.6580] },
          properties: { name: '渋谷駅店', hours: '11:00〜22:00' }
        }
      ]
    }
  });

  // サークルレイヤーとして描画
  map.addLayer({
    id: 'shops-layer',
    type: 'circle',
    source: 'shops',
    paint: {
      'circle-radius': 8,
      'circle-color': '#E74C3C',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff'
    }
  });

  // ① レイヤーのフィーチャーをクリックしたときの処理
  map.on('click', 'shops-layer', (e) => {
    const feature = e.features[0];
    const coordinates = feature.geometry.coordinates.slice(); // 座標をコピー
    const { name, hours } = feature.properties;

    // ② ポップアップを座標に直接表示
    new maplibregl.Popup()
      .setLngLat(coordinates)
      .setHTML(`
        <strong>${name}</strong><br/>
        <span>営業時間: ${hours}</span>
      `)
      .addTo(map);
  });

});

実際の表示

See the Pen maplibre-usage-08 by watashi-xyz (@watashi-xyz) on CodePen.

ホバーイベントでカーソルを変更する:

クリッカブルな要素にカーソルを合わせたとき、見た目をポインターに変えるとUXが向上します。

// レイヤー上にカーソルが入ったときにポインターに変更
map.on('mouseenter', 'shops-layer', () => {
  map.getCanvas().style.cursor = 'pointer';
});

// レイヤーからカーソルが出たときにデフォルトに戻す
map.on('mouseleave', 'shops-layer', () => {
  map.getCanvas().style.cursor = '';
});

ホバーしたフィーチャーをハイライトする:

マウスオーバーしたポイントの色をリアルタイムに変えることで、インタラクティブな地図体験を実現できます。

let hoveredFeatureId = null;

map.on('mousemove', 'shops-layer', (e) => {
  if (e.features.length > 0) {
    // 直前にホバーしていたフィーチャーのハイライトを解除
    if (hoveredFeatureId !== null) {
      map.setFeatureState(
        { source: 'shops', id: hoveredFeatureId },
        { hover: false }
      );
    }
    hoveredFeatureId = e.features[0].id;

    // 新たにホバーしたフィーチャーにhover状態をセット
    map.setFeatureState(
      { source: 'shops', id: hoveredFeatureId },
      { hover: true }
    );
  }
});

map.on('mouseleave', 'shops-layer', () => {
  if (hoveredFeatureId !== null) {
    map.setFeatureState(
      { source: 'shops', id: hoveredFeatureId },
      { hover: false }
    );
  }
  hoveredFeatureId = null;
});

setFeatureStateについて: MapLibreが提供するフィーチャーへの動的な状態付与APIです。hover: trueのような任意のフラグをフィーチャーに紐付け、レイヤーのスタイル式(feature-state)と組み合わせることで、再描画コストを最小限に抑えながらインタラクティブな表現を実現できます。

複数地点(店舗一覧)やGeoJSONの読み込みと表示

多数のマーカーを表示する際は、Markerクラスを繰り返すのではなくGeoJSONソース+レイヤーを使う方法が推奨です。 DOM要素ベースのMarkerを大量生成するとレンダリングが重くなりますが、GeoJSONレイヤーはWebGLで一括描画するため、数千件のデータでも高速に動作します。

外部GeoJSONファイルを読み込んで表示する:

map.on('load', () => {
  // URLを指定して外部GeoJSONを直接読み込める
  map.addSource('stores', {
    type: 'geojson',
    data: '/data/stores.geojson', // 相対パスまたは絶対URL
    cluster: true,          // クラスタリングを有効化
    clusterMaxZoom: 14,     // このズーム以上でクラスターを展開
    clusterRadius: 50       // クラスターにまとめる半径(px)
  });

  // ① クラスター(まとまったポイント)の描画
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'stores',
    filter: ['has', 'point_count'], // クラスターフィーチャーのみ対象
    paint: {
      // ポイント数に応じてサークルの色を変える(式の活用)
      'circle-color': [
        'step', ['get', 'point_count'],
        '#51bbd6',  // 10件未満: 水色
        10, '#f1f075', // 10件以上: 黄色
        30, '#f28cb1'  // 30件以上: ピンク
      ],
      'circle-radius': [
        'step', ['get', 'point_count'],
        20,      // 10件未満: 半径20px
        10, 30,  // 10件以上: 半径30px
        30, 40   // 30件以上: 半径40px
      ]
    }
  });

  // ② クラスター内の件数ラベルを表示
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'stores',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'], // "12" や "1.2k" などに自動整形
      'text-size': 14,
      'text-font': ['Open Sans Bold']
    },
    paint: {
      'text-color': '#fff'
    }
  });

  // ③ 個別ポイント(クラスターに含まれていないもの)の描画
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'stores',
    filter: ['!', ['has', 'point_count']], // クラスターでないものを対象
    paint: {
      'circle-radius': 7,
      'circle-color': '#E74C3C',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff'
    }
  });

  // ④ クラスターをクリックするとズームインして展開
  map.on('click', 'clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
    const clusterId = features[0].properties.cluster_id;

    // クラスターの展開に最適なズームレベルを取得してフライ移動
    map.getSource('stores').getClusterExpansionZoom(clusterId, (err, zoom) => {
      if (err) return;
      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom: zoom
      });
    });
  });

});

配列データをGeoJSONに変換して動的に表示する:

APIから取得したJSON配列など、GeoJSON形式でないデータを動的に地図に反映するパターンです。

// APIなどから取得した店舗データの想定
const storeData = [
  { id: 1, name: '新宿店', lng: 139.6991, lat: 35.6896, category: 'flagship' },
  { id: 2, name: '池袋店', lng: 139.7101, lat: 35.7295, category: 'standard' },
  { id: 3, name: '上野店', lng: 139.7742, lat: 35.7141, category: 'standard' },
];

// ① 配列をGeoJSON形式に変換するヘルパー関数
function toGeoJSON(stores) {
  return {
    type: 'FeatureCollection',
    features: stores.map(store => ({
      type: 'Feature',
      id: store.id, // setFeatureStateを使う場合はidが必要
      geometry: {
        type: 'Point',
        coordinates: [store.lng, store.lat]
      },
      properties: {
        name: store.name,
        category: store.category
      }
    }))
  };
}

map.on('load', () => {
  map.addSource('stores', {
    type: 'geojson',
    data: toGeoJSON(storeData)
  });

  map.addLayer({
    id: 'stores-layer',
    type: 'circle',
    source: 'stores',
    paint: {
      'circle-radius': 8,
      // categoryプロパティの値によって色を変える
      'circle-color': [
        'match', ['get', 'category'],
        'flagship', '#E74C3C', // フラッグシップは赤
        'standard', '#3498DB', // スタンダードは青
        '#999' // それ以外はグレー(デフォルト)
      ],
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff'
    }
  });

  // ② データを動的に更新する(データ取得後などに呼び出す)
  // map.getSource('stores').setData(toGeoJSON(newStoreData));
});

実際の表示

See the Pen maplibre-usage-09 by watashi-xyz (@watashi-xyz) on CodePen.

setData()による動的更新について: map.getSource('stores').setData(新しいGeoJSON)を呼び出すだけで、地図上のデータをリアルタイムに差し替えられます。フィルタリングや検索結果の反映など、インタラクティブな地図アプリの構築に非常に便利なAPIです。

線・面・ルート・3D表示など高度な地図表現

MapLibreの真価が発揮されるのが、WebGLを活かした高度な地図表現です。ルート描画・エリア表示・3Dビジュアライゼーションといった表現は、Google MapsやLeafletでは実現が難しいか、追加ライブラリが必要になるものですが、MapLibreではネイティブ機能として実装できます。

LineStringでルート表示を実装する方法

ルート表示には、GeoJSONのLineStringタイプをソースに使い、lineレイヤーで描画するのが基本パターンです。 経路案内・移動履歴・交通路線など、あらゆる「線」の表現に応用できます。

基本的なルート表示:

map.on('load', () => {

  // ① LineStringのGeoJSONをソースとして追加
  map.addSource('route', {
    type: 'geojson',
    data: {
      type: 'Feature',
      geometry: {
        type: 'LineString',
        // 座標の配列。[経度, 緯度] の順で経路順に並べる
        coordinates: [
          [139.7671, 35.6812], // 東京駅
          [139.7454, 35.6785], // 有楽町
          [139.7322, 35.6658], // 新橋
          [139.7573, 35.6551], // 浜松町
          [139.7454, 35.6453], // 品川
        ]
      },
      properties: {}
    }
  });

  // ② lineレイヤーでルートを描画
  map.addLayer({
    id: 'route-layer',
    type: 'line',
    source: 'route',
    layout: {
      'line-join': 'round', // 頂点の結合スタイル(round | miter | bevel)
      'line-cap': 'round'   // 端点のスタイル(round | butt | square)
    },
    paint: {
      'line-color': '#3498DB', // 線の色
      'line-width': 5,         // 線の太さ(px)
      'line-opacity': 0.9      // 不透明度
    }
  });

});

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

破線・グラデーション・アニメーションなど高度なスタイリング:

// 破線スタイルのルート
map.addLayer({
  id: 'route-dashed',
  type: 'line',
  source: 'route',
  paint: {
    'line-color': '#E74C3C',
    'line-width': 4,
    // line-dasharrayで破線を定義 [実線部分, 空白部分] の比率
    'line-dasharray': [2, 1]
  }
});

// ズームレベルに応じて線幅を変化させる(式の活用)
map.addLayer({
  id: 'route-responsive',
  type: 'line',
  source: 'route',
  paint: {
    'line-color': '#2ECC71',
    // interpolate式: ズームレベル10で3px、ズームレベル18で12pxに補間
    'line-width': [
      'interpolate', ['linear'], ['zoom'],
      10, 3,
      18, 12
    ]
  }
});

実際の表示

See the Pen maplibre-usage-11 by watashi-xyz (@watashi-xyz) on CodePen.

アニメーションでルートを順番に描画する(経路アニメーション):

経路を時間をかけて描いていくアニメーションは、ルート案内UIや移動ログの可視化で効果的です。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});

map.on('load', () => {

  // 全座標点(ゴール地点まで)
  const fullCoordinates = [
    [139.7671, 35.6812],
    [139.7454, 35.6785],
    [139.7322, 35.6658],
    [139.7573, 35.6551],
    [139.7454, 35.6453],
  ];

  // ① 最初は始点のみのソースを追加
  map.addSource('animated-route', {
    type: 'geojson',
    data: {
      type: 'Feature',
      geometry: { type: 'LineString', coordinates: [fullCoordinates[0]] },
      properties: {}
    }
  });

  map.addLayer({
    id: 'animated-route-layer',
    type: 'line',
    source: 'animated-route',
    layout: {
      'line-cap': 'round',
      'line-join': 'round' // 角を丸くしたい場合はこれも追加すると綺麗です
    },
    paint: {
      'line-color': '#E74C3C',
      'line-width': 4
    }
  });

  // ② 一定間隔で座標を1点ずつ追加していく
  let currentIndex = 1;
  const drawRoute = setInterval(() => {
    if (currentIndex >= fullCoordinates.length) {
      clearInterval(drawRoute); // 全点描画したら終了
      return;
    }

    // ソースデータを更新して新しい座標を追加
    map.getSource('animated-route').setData({
      type: 'Feature',
      geometry: {
        type: 'LineString',
        coordinates: fullCoordinates.slice(0, currentIndex + 1)
      },
      properties: {}
    });

    currentIndex++;
  }, 300); // 300msごとに1点追加

});

実際の表示

See the Pen maplibre-usage-12 by watashi-xyz (@watashi-xyz) on CodePen.

OSRM・Mapbox Directions APIと連携して実際の道路ルートを表示する:

現実の道路に沿ったルートを表示するには、ルーティングAPIから座標列を取得してLineStringに渡します。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


async function fetchAndDrawRoute(start, end) {
  // OSRM Public APIを使ったルート取得(商用利用には自前サーバー推奨)
  const url = `https://router.project-osrm.org/route/v1/driving/`
    + `${start[0]},${start[1]};${end[0]},${end[1]}`
    + `?overview=full&geometries=geojson`;

  const res = await fetch(url);
  const data = await res.json();

  // APIレスポンスからLineStringの座標配列を取得
  const routeCoordinates = data.routes[0].geometry.coordinates;

  // ソースにセットして地図に描画
  if (map.getSource('route')) {
    // すでにソースが存在する場合はデータを更新
    map.getSource('route').setData({
      type: 'Feature',
      geometry: { type: 'LineString', coordinates: routeCoordinates },
      properties: {}
    });
  } else {
    map.addSource('route', {
      type: 'geojson',
      data: {
        type: 'Feature',
        geometry: { type: 'LineString', coordinates: routeCoordinates },
        properties: {}
      }
    });
    map.addLayer({
      id: 'route-layer',
      type: 'line',
      source: 'route',
      paint: { 'line-color': '#3498DB', 'line-width': 5 }
    });
  }
}

// 使用例: 東京駅 → 渋谷駅
map.on('load', () => {
  fetchAndDrawRoute(
    [139.7671, 35.6812], // 東京駅
    [139.7003, 35.6580]  // 渋谷駅
  );
});

実際の表示

See the Pen maplibre-usage-13 by watashi-xyz (@watashi-xyz) on CodePen.

Polygon / MultiPolygon の描画とスタイル設定

エリアの塗りつぶしや境界線の表示には、GeoJSONのPolygonタイプとfillレイヤーを組み合わせます。 配送エリアの可視化・行政区域の表示・ハザードマップなど、面(エリア)を表現する幅広い用途に対応できます。

基本的なポリゴン描画:

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


map.on('load', () => {

  // ① Polygonのソースを追加
  // 座標配列の最初と最後は同じ点にして多角形を閉じる
  map.addSource('area', {
    type: 'geojson',
    data: {
      type: 'Feature',
      geometry: {
        type: 'Polygon',
        coordinates: [[
          [139.740, 35.700],
          [139.780, 35.700],
          [139.780, 35.670],
          [139.740, 35.670],
          [139.740, 35.700], // 始点と同じ座標で閉じる
        ]]
      },
      properties: { name: '配送エリアA' }
    }
  });

  // ② fillレイヤーでエリアを塗りつぶす
  map.addLayer({
    id: 'area-fill',
    type: 'fill',
    source: 'area',
    paint: {
      'fill-color': '#3498DB',
      'fill-opacity': 0.3  // 半透明にして地図を透かす
    }
  });

  // ③ line レイヤーで境界線を描画(fillレイヤーは境界線を持たない)
  map.addLayer({
    id: 'area-outline',
    type: 'line',
    source: 'area',
    paint: {
      'line-color': '#2980B9',
      'line-width': 2
    }
  });

});

fillレイヤーは境界線を描画しません。 境界線を表示したい場合は、同じソースに対してlineレイヤーを別途追加するのが正しい実装パターンです。

実際の表示

See the Pen maplibre-usage-14 by watashi-xyz (@watashi-xyz) on CodePen.

複数エリアを一括表示(FeatureCollection + プロパティ連動スタイル):

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


map.on('load', () => {

  map.addSource('delivery-areas', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Polygon',
            coordinates: [[[139.740,35.700],[139.780,35.700],
              [139.780,35.670],[139.740,35.670],[139.740,35.700]]]
          },
          properties: { name: 'エリアA', status: 'active', fee: 0 }
        },
        {
          type: 'Feature',
          geometry: {
            type: 'Polygon',
            coordinates: [[[139.700,35.700],[139.740,35.700],
              [139.740,35.670],[139.700,35.670],[139.700,35.700]]]
          },
          properties: { name: 'エリアB', status: 'inactive', fee: 500 }
        }
      ]
    }
  });

  // statusプロパティの値でエリアの色を動的に切り替える
  map.addLayer({
    id: 'delivery-areas-fill',
    type: 'fill',
    source: 'delivery-areas',
    paint: {
      'fill-color': [
        'match', ['get', 'status'],
        'active',   '#2ECC71', // 配送可能: 緑
        'inactive', '#E74C3C', // 配送不可: 赤
        '#999'                 // デフォルト: グレー
      ],
      'fill-opacity': 0.35
    }
  });

  map.addLayer({
    id: 'delivery-areas-outline',
    type: 'line',
    source: 'delivery-areas',
    paint: { 'line-color': '#555', 'line-width': 1.5 }
  });

  // エリアをクリックして情報を表示
  map.on('click', 'delivery-areas-fill', (e) => {
    const { name, status, fee } = e.features[0].properties;
    const statusLabel = status === 'active' ? '配送可能' : '配送不可';
    const feeLabel = fee === 0 ? '無料' : `${fee}円`;

    new maplibregl.Popup()
      .setLngLat(e.lngLat)
      .setHTML(`
        <strong>${name}</strong><br/>
        ステータス: ${statusLabel}<br/>
        配送料: ${feeLabel}
      `)
      .addTo(map);
  });

});

実際の表示

See the Pen maplibre-usage-15 by watashi-xyz (@watashi-xyz) on CodePen.

穴あきポリゴン(MultiPolygon・内側の穴):

特定のエリアを除外した形状を描画したい場合は、座標配列を2つ以上渡します。1つ目が外周、2つ目以降が穴(除外エリア)になります。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


map.on('load', () => {

  map.addSource('donut-area', {
    type: 'geojson',
    data: {
      type: 'Feature',
      geometry: {
        type: 'Polygon',
        coordinates: [
          // ① 外周(反時計回りが標準的)
          [
            [139.720, 35.650], [139.800, 35.650], 
            [139.800, 35.710], [139.720, 35.710], 
            [139.720, 35.650]
          ],
          // ② 穴(時計回りが標準的)
          [
            [139.740, 35.700], [139.780, 35.700], 
            [139.780, 35.665], [139.740, 35.665], 
            [139.740, 35.700]
          ]
        ]
      },
      properties: {}
    }
  });

  // レイヤーを追加しないと表示されません!
  map.addLayer({
    id: 'donut-layer',
    type: 'fill', // 塗りつぶし
    source: 'donut-area',
    paint: {
      'fill-color': '#3498db', // 青色
      'fill-opacity': 0.5,      // 半透明
      'fill-outline-color': '#2980b9'
    }
  });
});

実際の表示

See the Pen maplibre-usage-16 by watashi-xyz (@watashi-xyz) on CodePen.

3D建物(Extrusion)・3D地形(Terrain)の実装手順

MapLibreの最も印象的な機能のひとつが、WebGLを活かした3D表現です。建物の立体表示(Extrusion)と地形の起伏表示(Terrain)を実装することで、ゲームエンジンのようなリアルな地図体験を構築できます。

3D建物(fill-extrusion)の実装:

OpenStreetMapには建物の高さ情報(heightプロパティ)が含まれており、これをfill-extrusionレイヤーで3D表示できます。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7004, 35.6580], // [経度, 緯度] の順序に注意!

  zoom: 12,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


map.on('load', () => {
  // ① 昼夜判定のロジック
  const hour = new Date().getHours();
  const isNight = hour < 6 || hour >= 18;
  const buildingColor = isNight ? '#1a1a2e' : '#ccc';

  // ② ラベルレイヤー(Symbol)の手前に挿入するためのID特定
  const layers = map.getStyle().layers;
  const firstSymbolId = layers.find(l => l.type === 'symbol')?.id;

  // ③ 3Dレイヤー追加
  // 注: source名が 'openmaptiles' であることを前提としています
  map.addLayer({
    id: '3d-buildings',
    source: 'openmaptiles', 
    'source-layer': 'building',
    type: 'fill-extrusion',
    minzoom: 14,
    paint: {
      'fill-extrusion-color': buildingColor, // ここで判定済みの色を入れる

      'fill-extrusion-height': [
        'coalesce',
        ['get', 'render_height'], // タイルセットによってはこのプロパティ名の場合あり
        ['get', 'height'],
        ['*', ['get', 'render_rank'], 10], // 代わりのプロパティ案
        ['*', ['get', 'levels'], 3],
        10 
      ],

      'fill-extrusion-base': [
        'coalesce', ['get', 'min_height'], ['get', 'render_min_height'], 0
      ],

      'fill-extrusion-opacity': 0.85
    }
  }, firstSymbolId);

  // カメラを動かす
  map.easeTo({ pitch: 50, bearing: -20, duration: 2000 });
});

実際の表示

See the Pen maplibre-usage-17 by watashi-xyz (@watashi-xyz) on CodePen.

独自のGeoJSONデータで3D建物を表示:

OSMのタイルに頼らず、自前の建物データを3D表示することも可能です。

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',

  // 渋谷駅を中心に表示
  center: [139.7670,35.6814], // [経度, 緯度] の順序に注意!

  zoom: 14,      // 街区レベルの拡大率
  pitch: 45,     // 45度傾けて立体的に見せる
  bearing: -17,  // 北から17度反時計回りに回転

  // ズームの範囲を制限(日本全体〜建物レベルまで)
  minZoom: 5,
  maxZoom: 20,

  // 日本周辺に移動範囲を制限
  maxBounds: [
    [122.0, 20.0], // 南西端 [経度, 緯度]
    [154.0, 50.0]  // 北東端 [経度, 緯度]
  ],

  // URLのハッシュに現在位置を同期(ブラウザバックで地図位置が戻る)
  hash: true
});


map.on('load', () => {
  map.easeTo({ pitch: 55, bearing: 30, zoom: 16 });

  map.addSource('custom-buildings', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Polygon',
            coordinates: [[[139.7670,35.6814],[139.7675,35.6814],
              [139.7675,35.6810],[139.7670,35.6810],[139.7670,35.6814]]]
          },
          properties: { height: 50, color: '#E74C3C', name: 'ビルA' }
        },
        {
          type: 'Feature',
          geometry: {
            type: 'Polygon',
            coordinates: [[[139.7678,35.6813],[139.7685,35.6813],
              [139.7685,35.6808],[139.7678,35.6808],[139.7678,35.6813]]]
          },
          properties: { height: 120, color: '#3498DB', name: 'ビルB' }
        }
      ]
    }
  });

  map.addLayer({
    id: 'custom-buildings-layer',
    type: 'fill-extrusion',
    source: 'custom-buildings',
    paint: {
      // propertiesのcolorプロパティを色に使う
      'fill-extrusion-color': ['get', 'color'],
      'fill-extrusion-height': ['get', 'height'],
      'fill-extrusion-base': 0,
      'fill-extrusion-opacity': 0.9
    }
  });

});

実際の表示

See the Pen maplibre-usage-18 by watashi-xyz (@watashi-xyz) on CodePen.

3D地形(Terrain)の実装:

地形の起伏を3Dで表示するには、標高データ(DEM:Digital Elevation Model)を持つラスタータイルソースをterrainに設定します。 山岳地帯の地図や登山ルート表示に絶大な効果を発揮します。

map.on('load', () => {

  // ① 標高タイル(DEM)をラスタータイルソースとして追加
  // MapTilerの標高タイルを使用(要APIキー取得: maptiler.com)
  map.addSource('terrain-dem', {
    type: 'raster-dem',
    url: '<https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=YOUR_MAPTILER_KEY>',
    tileSize: 256
  });

  // ② terrainプロパティにDEMソースを指定して3D地形を有効化
  map.setTerrain({
    source: 'terrain-dem',
    exaggeration: 1.5 // 地形の誇張倍率(1.0が実際の高さ)
  });

  // ③ 空(スカイレイヤー)を追加するとよりリアルに見える
  map.addLayer({
    id: 'sky',
    type: 'sky',
    paint: {
      'sky-type': 'atmosphere',           // 大気散乱シミュレーション
      'sky-atmosphere-sun': [0.0, 90.0],  // 太陽の位置 [方位角, 仰角]
      'sky-atmosphere-sun-intensity': 15  // 太陽光の強度
    }
  });

  // ④ 山岳地帯を見渡す視点に設定
  map.jumpTo({
    center: [137.6503, 36.2901], // 立山周辺(富山県)
    zoom: 12,
    pitch: 70,   // 大きく傾けて地形を強調
    bearing: 30
  });

});

3D地形+ルートの組み合わせ(登山ルートの可視化):

3D地形の上にLineStringのルートを重ねる際は、line-elevation-referenceの設定に注意が必要です。

// 3D地形が有効な状態でルートラインを追加
map.addSource('hiking-route', {
  type: 'geojson',
  data: {
    type: 'Feature',
    geometry: {
      type: 'LineString',
      // 標高付き座標: [経度, 緯度, 標高(m)]
      coordinates: [
        [137.6200, 36.2800, 1000],
        [137.6300, 36.2850, 1400],
        [137.6400, 36.2900, 1900],
        [137.6503, 36.2901, 2450],
      ]
    },
    properties: {}
  }
});

map.addLayer({
  id: 'hiking-route-layer',
  type: 'line',
  source: 'hiking-route',
  paint: {
    'line-color': '#E74C3C',
    'line-width': 4,
    'line-opacity': 0.9
  },
  layout: {
    'line-cap': 'round',
    'line-join': 'round'
  }
});

3D表現まとめ:各機能の対応表

機能レイヤータイプ主なユースケース
3D建物fill-extrusion都市景観・不動産・施設案内
3D地形terrain(特殊設定)登山・観光・防災マップ
スカイskyリアルな空の描画・昼夜表現
3Dルートline(標高付き座標)登山ルート・ドローン航路

パフォーマンスに関する注意: 3D建物と3D地形を同時に有効にするとGPU負荷が高まります。モバイルデバイスでの動作確認を必ず行い、exaggeration値やminzoomを調整してパフォーマンスとビジュアルのバランスをとってください。

よくある質問(FAQ)

MapLibreで地図を表示しようとしたが、地図が真っ白(または真っ黒)になって表示されない。なぜ?

原因として最も多いのは以下の3つです。

① CSSの読み込み漏れ

MapLibreのCSSを読み込んでいないと、地図コンテナの表示が崩れて真っ白になります。HTMLの<head>内、またはJSファイルの先頭で必ずインポートしてください。

<!-- CDNの場合 -->
<link rel="stylesheet" href="<https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css>" />
// npmの場合
import 'maplibre-gl/dist/maplibre-gl.css';

② コンテナ要素に高さが指定されていない

#map要素はCSSで高さを明示しないとデフォルトでheight: 0になり、地図が描画されません。

#map {
  width: 100%;
  height: 100vh; /* または固定値: height: 500px; */
}

③ styleプロパティのURLが無効または到達不能

styleに指定したタイルスタイルのURLが間違っている、またはネットワークからアクセスできない場合、地図は真っ黒になります。ブラウザの開発者ツール(DevTools)のNetworkタブでスタイルJSONの取得が成功しているかを確認してください。

map.addLayer()map.addSource()を呼び出すと「Style is not done loading」というエラーが出る。どう対処すればいい?

A. map.on('load', ...)のコールバック内でレイヤー追加処理を行うことで解決します。

MapLibreはスタイルJSONや初期タイルの読み込みを非同期で行います。そのため、地図の初期化直後にaddSource()addLayer()を呼び出すと、スタイルがまだ準備できておらずエラーになります。

// ❌ 誤り: Mapの初期化直後に呼び出すとエラーになることがある
const map = new maplibregl.Map({ ... });
map.addSource('my-source', { ... }); // Style is not done loading エラー

// ✅ 正しい: 'load'イベントの発火後に呼び出す
const map = new maplibregl.Map({ ... });
map.on('load', () => {
  map.addSource('my-source', { ... });
  map.addLayer({ ... });
});

すでにスタイルが読み込み済みかどうかを確認してから処理したい場合は、map.isStyleLoaded()を使って条件分岐できます。

function addLayerSafely(map) {
  if (map.isStyleLoaded()) {
    // すでにロード済みなら即座に実行
    map.addLayer({ ... });
  } else {
    // まだ読み込み中なら'load'イベントを待つ
    map.once('load', () => {
      map.addLayer({ ... });
    });
  }
}

ononceの違い: map.on('load', fn)はイベントが発火するたびにfnを呼び出しますが、map.once('load', fn)初回の1度だけ呼び出します。スタイルの切り替えなどでloadが複数回発火するケースではonceを使うと安全です。

大量のマーカーを表示するとページが重くなる。パフォーマンスを改善するには?

maplibregl.Markerの大量生成をやめ、GeoJSON+レイヤー方式に切り替えることが最も効果的です。

maplibregl.MarkerはHTMLのDOM要素として地図上に配置されます。数十個程度であれば問題ありませんが、数百〜数千件になるとDOMの生成・管理コストが膨大になりブラウザが重くなります。

GeoJSONソース+circleレイヤー(またはsymbolレイヤー)を使う方式はWebGLで一括描画されるため、数万件のデータでも高速に動作します。

// ❌ 避けるべき: 大量のMarkerインスタンスを生成するパターン
const stores = [ /* 1000件のデータ */ ];
stores.forEach(store => {
  new maplibregl.Marker()
    .setLngLat([store.lng, store.lat])
    .addTo(map); // DOM要素を1000個生成 → 重くなる
});

// ✅ 推奨: GeoJSONレイヤーでWebGL描画するパターン
map.addSource('stores', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: stores.map(store => ({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [store.lng, store.lat] },
      properties: { name: store.name }
    }))
  }
});

map.addLayer({
  id: 'stores-layer',
  type: 'circle', // WebGLで1万件でも軽快に描画
  source: 'stores',
  paint: { 'circle-radius': 6, 'circle-color': '#E74C3C' }
});

さらに件数が多い場合(数万件以上)は、前述のクラスタリングcluster: true)を有効にするとズームレベルに応じてポイントが自動的にまとめられ、視認性とパフォーマンスの両方が向上します。

また、画面外のデータを描画しないようにするにはmaxBoundsで表示範囲を制限するか、map.getBounds()で現在のビューポートに含まれるデータだけをフィルタリングしてソースに渡す方法も有効です。

MapLibreで現在地(ユーザーの位置情報)を地図上に表示するにはどうすればいい?

MapLibreの組み込みコントロールGeolocateControlを追加するだけで実装できます。

MapLibreには位置情報取得のUIを提供するGeolocateControlが標準で用意されており、数行のコードで現在地表示を実装できます。

const map = new maplibregl.Map({
  container: 'map',
  style: '<https://tile.openstreetmap.jp/styles/osm-bright/style.json>',
  center: [139.6917, 35.6895],
  zoom: 12
});

// GeolocateControlを追加
const geolocate = new maplibregl.GeolocateControl({
  positionOptions: {
    enableHighAccuracy: true // GPSを使った高精度測位を有効化
  },
  trackUserLocation: true,  // リアルタイムで位置を追跡し続ける
  showUserHeading: true      // 端末の向きを矢印で表示
});

// 地図の右上にコントロールを追加
map.addControl(geolocate, 'top-right');

// 地図読み込み完了後に自動的に現在地取得を開始する場合
map.on('load', () => {
  geolocate.trigger(); // ボタンクリックなしで自動起動
});

// 位置情報取得成功時のコールバック
geolocate.on('geolocate', (e) => {
  const { longitude, latitude, accuracy } = e.coords;
  console.log(`現在地: 経度 ${longitude}, 緯度 ${latitude}`);
  console.log(`精度: ±${accuracy}m`);
});

// 位置情報取得エラー時のコールバック
geolocate.on('error', (e) => {
  console.error('位置情報の取得に失敗しました:', e.message);
  // ユーザーへのフォールバック処理をここに記述
});

GeolocateControlの主なオプション:

オプション説明
enableHighAccuracytrueでGPS優先の高精度測位(バッテリー消費増)
trackUserLocationtrueで移動に追従してリアルタイム更新
showUserHeadingtrueでコンパス方向を矢印表示(モバイル端末向け)
showAccuracyCirclefalseで測位精度の円を非表示(デフォルト: true)

HTTPS必須: ブラウザのGeolocation APIはセキュリティ上の理由からHTTPSのページでのみ動作します(localhostは例外)。本番環境へのデプロイ時はSSL証明書の設定を確認してください。

まとめ

ここまで、MapLibre GL JSの基本から実践的な応用まで、幅広く解説してきました。

MapLibreはMapbox GL JSからフォークされた完全OSSのライブラリで、BSDライセンスのもと商用プロジェクトでも無料で使えます。WebGLによる高速レンダリング・ベクタータイル対応・3D表現など、モダンな地図機能をすべて無料で手に入れられるのは、開発コストを抑えたいチームにとって非常に大きなメリットです。

導入もシンプルで、CDNを使えばHTMLファイル1枚で地図を表示できますし、npm経由でReactやVueにも自然に組み込めます。マーカーやポップアップ・GeoJSONによる大量データの描画・ルート表示・3D建物・3D地形まで、実務で必要になるほぼすべての地図表現をネイティブ機能でカバーできます。

重要ポイント

  • 商用利用が完全無料:BSDライセンスなので、サービスの収益化に一切制限がない
  • WebGLで高パフォーマンス:数万件のデータもGeoJSONレイヤーを使えば軽快に動作する
  • タイルデータは別途選定が必要:国内用途なら国土地理院タイル、グローバル対応ならMapTilerが選択肢
  • Reactでの実装はuseRef+クリーンアップが鉄則useStateの使用やアンマウント時のremove()漏れに注意
  • レイヤー追加は必ずmap.on('load', ...)内で:非同期読み込みを意識した実装でエラーを防ぐ
  • 大量マーカーはMarkerクラスではなくGeoJSONレイヤーで:DOMベースの大量生成はパフォーマンスの大敵

Leafletと比べると学習コストは少し高めですが、一度使い方を覚えてしまえば、表現力の高さと実装のシンプルさに手応えを感じるはずです。3D地形や建物の立体表示など、「地図でここまでできるのか」という体験は、ユーザーへの訴求力という点でも大きな武器になります。

まずはCDNの最小構成サンプルをコピーして、ブラウザで動かすところから始めてみてください。実際に手を動かすことが、MapLibre習得への一番の近道です。

タイトルとURLをコピーしました