1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】ZENRIN Maps APIのポイントスプライトで大量POIを高速表示する(CSV読み込み・ポップアップ付)

Last updated at Posted at 2025-09-19

はじめに

CSV などで扱う大量データを、もっと軽快に地図上へ表示したいと思ったことはないでしょうか。
今回は、ZENRIN Maps API(JavaScript)のPointSpriteクラスを使い、避難所データを例に大量ポイントを軽量に描画する実装手順を段階的にご紹介します。
内容は、ローダーの基本から始まり、地図の初期化・コントロール追加、CSV(Shift_JIS 想定)の読み込み、ポイントスプライトの描画、クリック時のポップアップ表示までを一歩ずつ追っていきます。

この記事でできること

  • 東京駅を中心に地図を表示
  • CSV(避難所データ)を読み込み、PointSpriteで大量ポイントを描画
  • クリックするとPopupに主要属性(名称/住所/種別)を表示
  • 地図のZoomButton / Compass / ScaleBarを追加

利用した外部データ

APIキー取得手順

ZENRIN Maps API を利用するには、事前に APIキーの取得が必要です。
現在、ZENRIN Maps API は2か月間の無料トライアルが用意されており、期間中は主要な機能を実際にお試しいただけます。
開発や評価の初期段階でも安心してご利用いただけます。
APIキーの取得方法については、以下の記事で詳しく解説されています。
初めての方は、まずこちらをご覧いただき、APIキーの発行と設定を行ってください。
ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成(例)

project/
├── index.html          メインHTMLサンプルコード
├── css/
   └── zma_pointsprite.css   スタイルシート
├── csv/
   └── hinan_0904.csv  震度データCSV形式
├── js/
   └── zma_pointsprite.js

ZENRIN Maps APIで地図上に大量のPOIを表示するサンプルコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ポイントスプライト地図表示</title>
  <link rel="stylesheet" href="style.css">

  <!-- Zenrin Maps API -->
  <script src="https://test-js.zmaps-api.com/zma_loader.js?key=[APIキー]&auth=referer"></script>
  <script src="script.js" defer></script>
</head>
<body>
  <div id="container">
    <div id="ZMap"></div>
    <div id="loading" class="loading" style="display: none;">読み込み中...</div>
  </div>
</body>
</html>
zma_pointsprite.css
body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: Arial, sans-serif;
}

#container {
  width: 100%;
  height: 100vh;
  position: relative;
}

#ZMap {
  width: 100%;
  height: 100%;
  z-index: 0;
  position: absolute;
  left: 0;
  top: 0;
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(255, 255, 255, 0.9);
  padding: 20px;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
  z-index: 1000;
}
zma_pointsprite.js
// グローバル変数
let map;
let widgets = [];
let json = [];
let json_psdata = [];

// ZMAローダーの初期化
ZMALoader.setOnLoad(function(mapOptions, error) {
  if (error) {
    console.error("ZMAローダーエラー:", error);
    return;
  }

  // 東京駅を中心に地図を初期化
  mapOptions.center = new ZDC.LatLng(35.6814, 139.7671);
  mapOptions.zoom = 13;
  mapOptions.mouseWheelReverseZoom = true;

  showLoading("地図読み込み中...");

  map = new ZDC.Map(document.getElementById('ZMap'), mapOptions,
    function() {
      hideLoading();

      // コントロール追加
      map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
      map.addControl(new ZDC.Compass('top-right'));
      map.addControl(new ZDC.ScaleBar('bottom-left'));

      // CSVデータを読み込み、ポイントスプライト表示
      loadSampleData();
      addInfoWidget();
    },
    function(error) {
      console.error('マップの読み込みに失敗しました:', error);
      hideLoading();
    }
  );
});

// CSVデータの読み込み
function loadSampleData() {
  showLoading("CSVデータ読み込み中...");

  fetch('./csv/hinan_0904.csv')
    .then(response => {
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      return response.arrayBuffer();
    })
    .then(buffer => {
      const decoder = new TextDecoder('shift_jis');
      const csvText = decoder.decode(buffer);
      const lines = csvText.split('\n');
      const data = [];

      function parseCSVLine(line) {
        const result = [];
        let current = '';
        let inQuotes = false;

        for (let char of line) {
          if (char === '"') {
            inQuotes = !inQuotes;
          } else if (char === ',' && !inQuotes) {
            result.push(current.trim());
            current = '';
          } else {
            current += char;
          }
        }
        result.push(current.trim());
        return result;
      }

      for (let i = 1; i < Math.min(lines.length, 150001); i++) {
        if (lines[i].trim()) {
          const values = parseCSVLine(lines[i]);
          const lat = parseFloat(values[1]);
          const lon = parseFloat(values[2]);
          if (!isNaN(lat) && !isNaN(lon)) {
            data.push({
              id: values[0],
              lat: lat,
              lon: lon,
              name: values[9] || '避難所',
              address: values[19] || '',
              type: values[11] || '避難所'
            });
          }
        }
      }

      json = data;
      json_psdata = data.map((item, i) => {
        item.psid = i + 1;
        return [i + 1, item.lat, item.lon];
      });

      addPointSprite();
      addInfoWidget();
      hideLoading();
    })
    .catch(error => {
      console.error('CSVファイル読み込みエラー:', error);
      hideLoading();
      alert('CSVファイルの読み込みに失敗しました: ' + error.message);
    });
}

// ポイントスプライトの追加
function addPointSprite() {
  const options = {
    size: 12,
    color: [0.0, 0.6, 1.0, 0.8], // 青色
    propagation: true
  };

  try {
    const ps = new ZDC.PointSprite(json_psdata, options);
    widgets.push(ps);

    ps.addEventListener('click', function(e) {
      const psid = ps.getPSID(e.point);
      const targetData = getPSIDData(psid);

      if (targetData) {
        const html = makeHTML(targetData);
        const ll = new ZDC.LatLng(targetData.lat, targetData.lon);
        const popup = new ZDC.Popup(ll, { htmlSource: html });
        map.addWidget(popup);
      }
    });

    map.addWidget(ps);
  } catch (error) {
    console.error("ポイントスプライトエラー:", error);
    hideLoading();
  }
}

// PSIDからデータ取得
function getPSIDData(psid) {
  return json.find(item => item.psid === psid) || null;
}

// ポップアップHTML生成
function makeHTML(data) {
  const table = document.createElement('table');
  table.style.borderCollapse = 'collapse';
  table.style.width = '100%';
  table.style.fontSize = '14px';
  table.style.border = '1px solid #ccc';

  const fieldNames = { name: '避難所名', address: '住所', type: '種別' };
  const displayFields = ['name', 'address', 'type'];

  displayFields.forEach(key => {
    if (data[key]) {
      const row = table.insertRow();
      const cell1 = row.insertCell(0);
      const cell2 = row.insertCell(1);

      cell1.textContent = fieldNames[key];
      cell1.style.fontWeight = 'bold';
      cell1.style.border = '1px solid #ddd';
      cell1.style.padding = '6px';
      cell1.style.backgroundColor = '#f5f5f5';

      cell2.textContent = data[key];
      cell2.style.border = '1px solid #ddd';
      cell2.style.padding = '6px';
    }
  });

  return table.outerHTML;
}

// 情報ウィジェット
function addInfoWidget() {
  const infoHtml = `
    <div style="background:#FFF;color:#000;font-size:12pt;padding:10px;border-radius:5px;">
      <strong>避難所マップ</strong><br>
      青いポイントをクリックすると避難所情報が表示されます。<br>
      表示件数: ${json_psdata.length}件
    </div>
  `;
  const infoWidget = new ZDC.StaticUserWidget(new ZDC.Point(10, 10), { html: infoHtml });
  map.addWidget(infoWidget);
}

// ローディング表示制御
function showLoading(message) {
  const loading = document.getElementById('loading');
  if (loading) {
    loading.textContent = message;
    loading.style.display = 'block';
  }
}

function hideLoading() {
  const loading = document.getElementById('loading');
  if (loading) loading.style.display = 'none';
}

コードを実行した結果は、以下になります。
地図を移動しても、多数のポイントを素早く描画できます。
zma_pointsprite2.gif
青いポイントをクリックすることで、情報を表示することが可能です。
pointsprite_sample2.png

実装ステップ(解説付き)

Step 1: HTMLの準備

<!-- Zenrin Maps API -->
  <script src="https://test-js.zmaps-api.com/zma_loader.js?key=[APIキー]&auth=referer"></script>
  <script src="js/zma_script.js" defer></script>
</head>
<body>
  <div id="container">
    <div id="ZMap"></div>
    <div id="loading" class="loading" style="display: none;">読み込み中...</div>
  </div>
</body>
  • zma_loader.js の URL に APIキー と 認証方式(auth=referer など) を反映する必要があります。
  • #ZMap は地図ウィジェットを描画する DOM。#loading は読み込み中表示用です。
  • defer を付けた script.js によって HTML パース後に JS が実行されます。

Step 2: CSSでレイアウト調整

#ZMap {
  width: 100%;
  height: 100%;
  z-index: 0;
  position: absolute;
  left: 0;
  top: 0;
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(255,255,255,0.9);
  padding: 20px;
  border-radius: 5px;
  z-index: 1000;
}

  • #ZMapposition:absolute でフルスクリーンにすることで地図が画面いっぱいに表示されます。
  • .loadingshowLoading() / hideLoading() で表示を制御します。

Step 3:ローダーの完了を待って地図を初期化する

ZMALoader.setOnLoad(function(mapOptions, error) {
  if (error) {
    console.error("ZMAローダーエラー:", error);
    return;
  }

  // 地図中心・ズームを設定(東京駅)
  mapOptions.center = new ZDC.LatLng(35.6814, 139.7671);
  mapOptions.zoom = 13;
  mapOptions.mouseWheelReverseZoom = true;

  showLoading("地図読み込み中...");

  map = new ZDC.Map(document.getElementById('ZMap'), mapOptions,
    function() { /* 成功時コールバック */ },
    function(error) { /* エラー時コールバック */ }
  );
});
  • ZMALoader.setOnLoad のコールバックはローダー初期化完了後に呼ばれるため、ここで mapOptions に中心座標・ズームを上書きします。
  • new ZDC.Map(container, options, onSuccess, onError) の成功コールバックでコントロール追加やデータ読み込みを呼びます。

Step 4:地図上のコントロールを追加する

// 成功コールバック内
map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
map.addControl(new ZDC.Compass('top-right'));
map.addControl(new ZDC.ScaleBar('bottom-left'));
  • ZoomButton, Compass, ScaleBar はよく使う UI 要素。位置は 'top-right' などで指定します。

Step 5:CSV を読み込み、Shift_JIS を UTF-8 に変換する

fetch('./csv/hinan_0904.csv')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return response.arrayBuffer();
  })
  .then(buffer => {
    const decoder = new TextDecoder('shift_jis');
    const csvText = decoder.decode(buffer);
    // csvText を改行で分割してパースへ
  })

  • fetcharrayBuffer()TextDecoder('shift_jis').decode() の流れで、Shift_JIS の CSV を扱えます。
  • file:// での読み込みは制限があるため、簡易サーバ(npx serve など)でホストしてください。

Step 6:CSV を安全にパースする

function parseCSVLine(line) {
  const result = [];
  let current = '';
  let inQuotes = false;

  for (let i = 0; i < line.length; i++) {
    const char = line[i];
    if (char === '"') {
      inQuotes = !inQuotes;
    } else if (char === ',' && !inQuotes) {
      result.push(current.trim());
      current = '';
    } else {
      current += char;
    }
  }
  result.push(current.trim());
  return result;
}
  • 基本的な "..." 内のカンマを無視する処理は対応しています。
  • 実運用で特殊ケースが多い場合は既存の CSV ライブラリ導入を検討してください。

Step 7:緯度経度を検証してデータ配列を作る

for (let i = 1; i < Math.min(lines.length, 30001); i++) {
  if (lines[i].trim()) {
    const values = parseCSVLine(lines[i]);
    const lat = parseFloat(values[1]);
    const lon = parseFloat(values[2]);
    if (!isNaN(lat) && !isNaN(lon)) {
      data.push({
        id: values[0],
        lat: lat,
        lon: lon,
        name: values[9] || '避難所',
        address: values[19] || '',
        type: values[11] || '避難所'
      });
    }
  }
}
  • サンプルでは values[1] を緯度、values[2] を経度として扱っています。CSV の列順に合わせてインデックスを調整してください。
  • ループ上限 30001 は安全策(最大 3 万件)です。必要に応じて調整をしてください。

Step 8:CSVファイルをPointSprite 用データ([psid, lat, lon])に変換する

json = data;
json_psdata = data.map((item, i) => {
  item.psid = i + 1;
  return [i + 1, item.lat, item.lon];
});

  • psid はクリック時に元データを引くために付与します(getPSIDData(psid) で検索)。

Step 9:PointSprite を生成して地図に追加する

const options = { size: 12, color: [0.0, 0.6, 1.0, 0.8], propagation: true };
const ps = new ZDC.PointSprite(json_psdata, options);
map.addWidget(ps);
  • クリックイベント(PointSprite → Popup)
ps.addEventListener('click', function(e) {
  const psid = ps.getPSID(e.point);
  const targetData = getPSIDData(psid);
  if (targetData) {
    const html = makeHTML(targetData);
    const ll = new ZDC.LatLng(targetData.lat, targetData.lon);
    const popup = new ZDC.Popup(ll, { htmlSource: html });
    map.addWidget(popup);
  }
});

  • ZDC.PointSprite は大量の点を効率的にレンダリングします。
  • ps.getPSID(e.point) でクリックされた点の psid を取得し、元データを参照して Popup を作ります。

Step 10:Popup(ポップアップ)の HTML を生成する

function makeHTML(data) {
  const table = document.createElement('table');
  // ... テーブルスタイル設定 ...
  const fieldNames = { name: '避難所名', address: '住所', type: '種別' };
  const displayFields = ['name', 'address', 'type'];
  displayFields.forEach(key => {
    if (data[key]) {
      const row = table.insertRow();
      const c1 = row.insertCell(0);
      const c2 = row.insertCell(1);
      c1.textContent = fieldNames[key];
      c2.textContent = data[key];
    }
  });
  return table.outerHTML;
}

DOM API を用いて安全にテーブルを組み立てます(直接 innerHTML を使うより事故が少ないため)。

Step 11:情報ウィジェットで地図上に説明を置く

function addInfoWidget() {
  const infoHtml = `
    <div style="background:#FFF;color:#000;font-size:12pt;padding:10px;border-radius:5px;">
      <strong>避難所マップ</strong><br>
      青いポイントをクリックすると避難所情報が表示されます。<br>
      表示件数: ${json_psdata.length}件
    </div>
  `;
  const infoWidget = new ZDC.StaticUserWidget(new ZDC.Point(10, 10), { html: infoHtml });
  map.addWidget(infoWidget);
}
  • StaticUserWidget に HTML を与えて地図の特定座標に固定表示します(ここでは UI 座標指定)。

Step 12:ローディング表示の切替

function showLoading(message) {
  const loading = document.getElementById('loading');
  if (loading) {
    loading.textContent = message;
    loading.style.display = 'block';
  }
}
function hideLoading() {
  const loading = document.getElementById('loading');
  if (loading) loading.style.display = 'none';
}
  • 読み込み中はユーザーにその旨を見せ、エラー時は catchalert を出すなど方針を統一しています。

おわりに

PointSprite クラスを使い、大量ポイントを軽量に描画する方法をご紹介しました。
「大量のデータも PointSprite で軽快に扱える」ことを実感していただけたのではないでしょうか。
今回のサンプルは避難所データを例にしましたが、物流拠点の管理、店舗分布の分析、観光地の可視化など、さまざまな業務シーンに応用できます。
ぜひ実際のデータに置き換えて試し、皆さんのシーンに合った地図活用を広げていただければ幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?