Vue.jsアプリケーションにおける位置情報取得と座標変換の実装

はじめに

現代のウェブアプリケーションでは、ユーザーの位置情報を活用する場面が多々あります。地図表示、近隣店舗検索、位置に基づくサービスの提供など、その用途は多岐にわたります。本稿では、Vue.jsアプリケーションにおいて、ブラウザのネイティブAPIと高徳地図(Amap)APIを利用して位置情報を取得し、さらに中国特有の座標系であるGCJ02への変換を含む、位置情報処理の実装について解説します。

座標系の理解と変換ユーティリティ

位置情報を扱う上で重要なのは、使用されている座標系を理解することです。一般的にGPSデバイスから取得される座標は世界測地系(WGS84)ですが、中国本土では国家測量基準に基づき、独自の測地系であるGCJ02(火星座標系)の使用が義務付けられています。そのため、WGS84座標を中国国内の地図サービスで正確に表示するには、GCJ02への変換が必要になります。

以下に、WGS84座標をGCJ02座標に変換するためのJavaScriptユーティリティ関数を示します。この変換は、特定の数学的アルゴリズムに基づいて行われます。

座標変換ユーティリティ (coordinate-transformer.js)


// 定数定義 (変換アルゴリズムに依存)
const MATH_PI = Math.PI;
const EARTH_SEMI_MAJOR_AXIS = 6378245.0; // 地球の長半径 (WGS84)
const ELLIPSOID_ECCENTRICITY_SQUARED = 0.00669342162296594323; // 扁平率の二乗

/**
 * 指定された経緯度が中国本土の境界外にあるかを判定します。
 * 中国本土外であればオフセットを適用しません。
 * @param {Number} longitude - 経度
 * @param {Number} latitude - 緯度
 * @returns {Boolean} 中国本土外であれば true
 */
function isOutsideChinaRegion(longitude, latitude) {
  return (
    longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271
  );
}

// ラジアンから度への変換
function radiansToDegrees(radians) {
  return (radians * 180) / MATH_PI;
}

// 度からラジアンへの変換
function degreesToRadians(degrees) {
  return (degrees * MATH_PI) / 180;
}

// 緯度オフセットの計算ロジック
function calculateLatitudeOffset(xCoord, yCoord) {
  let latOffset = -100.0 + 2.0 * xCoord + 3.0 * yCoord + 0.2 * yCoord * yCoord + 0.1 * xCoord * yCoord + 0.2 * Math.sqrt(Math.abs(xCoord));
  latOffset += (20.0 * Math.sin(6.0 * xCoord * MATH_PI) + 20.0 * Math.sin(2.0 * xCoord * MATH_PI)) * 2.0 / 3.0;
  latOffset += (20.0 * Math.sin(yCoord * MATH_PI) + 40.0 * Math.sin(yCoord / 3.0 * MATH_PI)) * 2.0 / 3.0;
  latOffset += (160.0 * Math.sin(yCoord / 12.0 * MATH_PI) + 320 * Math.sin(yCoord * MATH_PI / 30.0)) * 2.0 / 3.0;
  return latOffset;
}

// 経度オフセットの計算ロジック
function calculateLongitudeOffset(xCoord, yCoord) {
  let lngOffset = 300.0 + xCoord + 2.0 * yCoord + 0.1 * xCoord * xCoord + 0.1 * xCoord * yCoord + 0.1 * Math.sqrt(Math.abs(xCoord));
  lngOffset += (20.0 * Math.sin(6.0 * xCoord * MATH_PI) + 20.0 * Math.sin(2.0 * xCoord * MATH_PI)) * 2.0 / 3.0;
  lngOffset += (20.0 * Math.sin(xCoord * MATH_PI) + 40.0 * Math.sin(xCoord / 3.0 * MATH_PI)) * 2.0 / 3.0;
  lngOffset += (150.0 * Math.sin(xCoord / 12.0 * MATH_PI) + 300.0 * Math.sin(xCoord / 30.0 * MATH_PI)) * 2.0 / 3.0;
  return lngOffset;
}

/**
 * WGS84座標をGCJ02(火星座標系)に変換します。
 * 中国本土外の座標は変換されません。
 * @param {Number} wgsLng - WGS84経度
 * @param {Number} wgsLat - WGS84緯度
 * @returns {Object} 変換されたGCJ02座標 { longitude, latitude }
 */
export function convertWgs84ToGcj02(wgsLng, wgsLat) {
  // 中国本土外であればオフセットを適用しない
  if (isOutsideChinaRegion(wgsLng, wgsLat)) {
    return { longitude: wgsLng, latitude: wgsLat };
  }

  let deltaLat = calculateLatitudeOffset(wgsLng - 105.0, wgsLat - 35.0);
  let deltaLng = calculateLongitudeOffset(wgsLng - 105.0, wgsLat - 35.0);
  const radLatitude = degreesToRadians(wgsLat);
  let magicTerm = Math.sin(radLatitude);
  magicTerm = 1 - ELLIPSOID_ECCENTRICITY_SQUARED * magicTerm * magicTerm;
  const sqrtMagicTerm = Math.sqrt(magicTerm);

  deltaLat = (deltaLat * 180.0) / ((EARTH_SEMI_MAJOR_AXIS * (1 - ELLIPSOID_ECCENTRICITY_SQUARED)) / (magicTerm * sqrtMagicTerm) * MATH_PI);
  deltaLng = (deltaLng * 180.0) / (EARTH_SEMI_MAJOR_AXIS / sqrtMagicTerm * Math.cos(radLatitude) * MATH_PI);

  const gcjLat = wgsLat + deltaLat;
  const gcjLng = wgsLng + deltaLng;

  return { longitude: gcjLng, latitude: gcjLat };
}

ブラウザネイティブのGeolocation APIを用いた位置情報取得

多くのモダンブラウザは、navigator.geolocationオブジェクトを通じて、デバイスの地理位置情報を取得する機能を提供しています。この方法は、外部のAPIキーが不要で手軽に利用できる反面、ユーザーによる許可が必要であり、精度や取得速度はデバイスや環境に依存します。

Vueコンポーネントの例


<template>
  <div>
    <button @click="fetchDeviceLocation" :disabled="isFetchingLocation">
      現在地を取得
    </button>
    <p v-if="isFetchingLocation">位置情報を取得中...</p>
    <p v-if="locationError">{{ locationError }}</p>
    <p v-if="locationData">
      経度: {{ locationData.longitude }},緯度: {{ locationData.latitude }}
    </p>
  </div>
</template>

<script>
import { convertWgs84ToGcj02 } from './coordinate-transformer'; // 変換関数をインポート

export default {
  name: 'NativeGeolocationComponent',
  data() {
    return {
      locationData: null, // 取得した位置情報 (経度、緯度など)
      locationError: null, // エラーメッセージ
      isFetchingLocation: false, // 位置情報取得中の状態
    };
  },
  methods: {
    fetchDeviceLocation() {
      if (this.isFetchingLocation) return;

      // ブラウザが地理位置情報APIをサポートしているか確認
      if (!navigator.geolocation) {
        this.locationError = 'お使いのブラウザは地理位置情報APIをサポートしていません。';
        return;
      }

      // 状態をリセット
      this.isFetchingLocation = true;
      this.locationError = null;
      this.locationData = null;

      // 位置情報の取得を開始
      navigator.geolocation.getCurrentPosition(
        (pos) => {
          const { latitude, longitude, accuracy } = pos.coords;
          // 成功時にWGS84座標を取得
          const wgs84Coords = { latitude, longitude, accuracy };
          // GCJ02座標に変換して格納
          this.locationData = convertWgs84ToGcj02(wgs84Coords.longitude, wgs84Coords.latitude);
          this.isFetchingLocation = false;
          console.log(`位置情報取得成功: 精度 ${wgs84Coords.accuracy}メートル`, this.locationData);
        },
        (err) => {
          this.isFetchingLocation = false;
          let errorMessage = '位置情報の取得中に不明なエラーが発生しました。';
          // エラーコードに基づいてメッセージを設定
          switch (err.code) {
            case 1:
              errorMessage = '位置情報へのアクセスが拒否されました。ブラウザの設定で許可してください。';
              break;
            case 2:
              errorMessage = '位置情報を取得できませんでした。GPSがオフになっている可能性があります。';
              break;
            case 3:
              errorMessage = '位置情報取得がタイムアウトしました。開けた場所でお試しください。';
              break;
          }
          this.locationError = errorMessage;
          this.locationData = null;
          console.error('位置情報取得エラー:', err);
        },
        {
          enableHighAccuracy: true, // 高精度モードを有効にする (GPSモジュールを使用)
          timeout: 8000,           // タイムアウト時間 (ミリ秒)
          maximumAge: 0            // キャッシュされた位置情報を使用しない (常に最新を取得)
        }
      );
    },
  },
};
</script>

高徳地図APIを利用した位置情報取得

ブラウザネイティブのGeolocation APIと比較して、高徳地図などの専門的な地図サービスAPIは、より高い精度や安定性、そしてより詳細な位置関連機能を提供することが多いです。特に中国国内では、その最適化されたサービスが有利に働きます。高徳地図APIを使用するには、まず高徳開放平台でAPIキーを取得する必要があります。

高徳地図Geocodingサービスに関する参考情報

Vueコンポーネントの例

このコンポーネントは、高徳地図のJavaScript APIを動的にロードし、そのGeolocationプラグインを使用して位置情報を取得します。取得した座標は、必要に応じてWGS84からGCJ02へ変換されます。


<template>
  <div>
    <button @click="retrieveLocation" :disabled="isRetrievingLocation">
      高徳地図で位置取得
    </button>
    <p v-if="isRetrievingLocation">高徳地図APIで位置情報を取得中...</p>
    <p v-if="errorOccurred">{{ errorOccurred }}</p>
    <!-- <p v-if="coordinatesResult">経度:{{ coordinatesResult.longitude }},緯度:{{ coordinatesResult.latitude }}</p> -->
  </div>
</template>

<script>
import { convertWgs84ToGcj02 } from '@/utils/coordinate-transformer'; // 座標変換ユーティリティをインポート

export default {
  name: 'AmapGeolocationService',
  data() {
    return {
      coordinatesResult: null,      // 取得した経緯度
      errorOccurred: null,          // エラーメッセージ
      isRetrievingLocation: false,  // 位置情報取得中の状態
      amapScriptReady: false,       // 高徳地図スクリプトのロード完了状態
      amapScriptLoading: false,     // 高徳地図スクリプトのロード中状態
      scriptElement: null,          // ロードしたスクリプト要素
      amapGeolocation: null,        // 高徳地図 Geolocation インスタンス
    };
  },
  mounted() {
    this.loadAmapScript();
  },
  methods: {
    // 高徳地図スクリプトを動的にロードする
    loadAmapScript() {
      // 既にロード済みか、ロード中であれば何もしない
      if (window.AMap || this.amapScriptLoading) {
        this.amapScriptReady = !!window.AMap;
        return;
      }

      this.amapScriptLoading = true;
      this.scriptElement = document.createElement('script');
      // 高徳地図APIのURL。"YOUR_AMAP_API_KEY_HERE" を実際のAPIキーに置き換えてください。
      this.scriptElement.src = 'https://webapi.amap.com/maps?v=1.4.15&key=YOUR_AMAP_API_KEY_HERE';
      this.scriptElement.type = 'text/javascript';

      // スクリプトロードのタイムアウト処理
      const scriptLoadTimeout = setTimeout(() => {
        this.amapScriptLoading = false;
        console.warn('高徳地図スクリプトのロードがタイムアウトしました。');
      }, 12000); // タイムアウトを12秒に設定

      // スクリプトロード成功時の処理
      this.scriptElement.onload = () => {
        clearTimeout(scriptLoadTimeout);
        this.amapScriptLoading = false;
        if (typeof AMap === 'undefined' || !AMap) {
          console.error('高徳地図APIのロードに失敗しました。AMapオブジェクトが見つかりません。');
        } else {
          this.amapScriptReady = true;
          console.log('高徳地図APIが正常にロードされました。');
        }
      };

      // スクリプトロード失敗時の処理
      this.scriptElement.onerror = () => {
        clearTimeout(scriptLoadTimeout);
        this.amapScriptLoading = false;
        console.error('高徳地図スクリプトのロード中にエラーが発生しました。ネットワークを確認してください。');
      };

      document.body.appendChild(this.scriptElement);
    },

    // 位置情報取得を開始する
    retrieveLocation() {
      if (this.isRetrievingLocation) return;

      this.isRetrievingLocation = true;
      this.errorOccurred = null;
      this.coordinatesResult = null;

      if (this.amapScriptReady && window.AMap) {
        this.initializeAmapGeolocation();
      } else if (this.amapScriptLoading) {
        this.errorOccurred = '地図APIを初期化中です。しばらくお待ちください...';
        // スクリプトのロードが完了するまで待機
        const checkReadyInterval = setInterval(() => {
          if (this.amapScriptReady) {
            clearInterval(checkReadyInterval);
            this.isRetrievingLocation = false;
            this.retrieveLocation(); // APIがロードされたら再試行
          } else if (!this.amapScriptLoading) {
            clearInterval(checkReadyInterval);
            this.isRetrievingLocation = false;
            this.errorOccurred = '地図APIの初期化に失敗しました。ページを再読み込みしてください。';
          }
        }, 500); // 500ms間隔でロード状況をチェック
      } else {
        // スクリプトがまだロードされていない、またはロードに失敗した
        this.isRetrievingLocation = false;
        this.errorOccurred = '地図APIのロードに失敗しました。再試行します...';
        this.loadAmapScript(); // スクリプトを再ロード
        setTimeout(() => this.retrieveLocation(), 1500); // 1.5秒後に位置取得を再試行
      }
    },

    // 高徳地図のGeolocation機能の初期化と位置取得
    initializeAmapGeolocation() {
      AMap.plugin('AMap.Geolocation', () => {
        this.amapGeolocation = new AMap.Geolocation({
          enableHighAccuracy: true,  // 高精度定位を有効にする
          timeout: 6000,             // タイムアウト時間 (ミリ秒)
          maximumAge: 0,             // キャッシュされた位置情報を使用しない
          convert: false             // 座標変換を行わない (高徳地図APIがデフォルトでGCJ02を返すため)
        });
        this.amapGeolocation.getCurrentPosition((status, result) => {
          console.log('高徳地図 定位結果:', JSON.stringify(result));
          this.isRetrievingLocation = false;
          if (status === 'complete' && result.status === 1) {
            const { lng, lat } = result.position;
            const fetchedCoords = {
              longitude: lng,
              latitude: lat,
              accuracy: result.accuracy,
            };

            // 高徳地図APIは通常GCJ02座標を返しますが、
            // HTML5またはGPSからの取得と示される場合はWGS84と見なし、GCJ02に変換します。
            if (['html5', 'gps'].includes(result.location_type)) {
              this.coordinatesResult = convertWgs84ToGcj02(fetchedCoords.longitude, fetchedCoords.latitude);
            } else {
              this.coordinatesResult = fetchedCoords; // 既にGCJ02と仮定
            }
            this.errorOccurred = null;
            console.log('位置取得成功:', this.coordinatesResult);
            // 親コンポーネントに結果をemit
            this.$emit('location-updated', this.coordinatesResult);
          } else {
            // エラー処理
            let msg = '位置情報の取得に失敗しました。';
            if (result && result.message) {
                 if (result.message.includes('sdk定位失败')) msg = 'SDK定位失敗。APIキーやデバイスの権限を確認してください。';
                 else if (result.message.includes('Browser not Support html5 geolocation')) msg = 'ブラウザがHTML5位置情報APIをサポートしていません。';
                 else if (result.message.includes('Geolocation permission denied')) msg = 'ユーザーが位置情報権限を拒否しました。';
                 else if (result.message.includes('Get geolocation time out')) msg = 'ブラウザ位置情報取得がタイムアウトしました。';
                 else if (result.message.includes('Get ipLocation failed')) msg = 'IP位置特定に失敗しました。';
                 else msg = '高徳地図APIエラー: ' + result.message;
            }
            this.errorOccurred = msg;
            this.coordinatesResult = null;
          }
        });
      });
    },
  },
  beforeDestroy() {
    // コンポーネントが破棄される前に、ロードしたスクリプトをDOMから削除
    if (this.scriptElement && this.scriptElement.parentNode) {
      this.scriptElement.parentNode.removeChild(this.scriptElement);
      console.log('高徳地図関連スクリプトをDOMから削除しました。');
    }
  },
};
</script>

逆ジオコーディング (経緯度から住所への変換)

位置情報を取得した後、その経緯度が具体的にどの住所に当たるかを知りたい場合があります。これを「逆ジオコーディング」と呼びます。高徳地図APIは、経緯度座標を人間が読める住所情報に変換する機能も提供しています。

以下は、Javaで高徳地図の逆ジオコーディングAPIを呼び出すサンプルコードです。

Javaでの逆ジオコーディングの例


package jp.example.location; // パッケージ名を変更

import org.json.JSONObject; // org.jsonライブラリを使用 (或は alibaba.fastjson など。。。)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets; // 文字エンコーディング用

public class AmapReverseGeocodingService { // クラス名を変更

    private static final String AMAP_API_BASE_URL = "https://restapi.amap.com/v3/geocode/regeo";

    public static void main(String[] args) {
        // 逆ジオコーディングを行う対象の経緯度
        BigDecimal targetLongitude = new BigDecimal("118.32739780938687");
        BigDecimal targetLatitude = new BigDecimal("35.081301798659695");
        // 高徳地図のAPIキー (自身のキーに置き換えてください)
        String amapApiKey = "YOUR_AMAP_API_KEY_HERE";

        try {
            // APIリクエストURLを構築
            String apiUrl = buildReverseGeocodingUrl(targetLongitude, targetLatitude, amapApiKey);
            // APIからレスポンスを取得
            String responseJson = fetchApiResponse(apiUrl);

            // APIレスポンスをJSONとして解析
            JSONObject jsonResponse = new JSONObject(responseJson);
            JSONObject regeocodeObject = jsonResponse.getJSONObject("regeocode");
            String formattedAddress = regeocodeObject.getString("formatted_address");

            // 取得した住所情報を出力
            System.out.println("取得した住所情報: " + formattedAddress);

        } catch (Exception e) {
            System.err.println("逆ジオコーディング処理中にエラーが発生しました。");
            e.printStackTrace();
        }
    }

    /**
     * 高徳地図の逆ジオコーディングAPIのURLを構築します。
     * @param longitude 経度
     * @param latitude 緯度
     * @param apiKey 高徳地図APIキー
     * @return 構築されたAPIリクエストURL
     */
    private static String buildReverseGeocodingUrl(BigDecimal longitude, BigDecimal latitude, String apiKey) {
        StringBuilder params = new StringBuilder();
        params.append("?key=").append(apiKey);
        params.append("&location=").append(longitude).append(",").append(latitude); // 経緯度座標
        params.append("&extensions=base"); // 返す結果の制御 (base: 基本情報のみ, all: 詳細情報も含む)
        params.append("&radius=50");       // 検索半径 (メートル単位、デフォルト1000m、ここでは50mに設定)
        params.append("&batch=false");     // バッチクエリではない (単一ポイントクエリ)
        params.append("&roadlevel=1");     // 道路レベル (0:すべて、1:主要道路のみなど、ここでは1に設定)

        return AMAP_API_BASE_URL + params.toString();
    }

    /**
     * 指定されたURLからAPIレスポンスを取得します。
     * @param requestUrl APIリクエストURL
     * @return APIからのJSONレスポンス文字列
     * @throws Exception HTTP接続または読み取りエラーの場合
     */
    private static String fetchApiResponse(String requestUrl) throws Exception {
        URL url = new URL(requestUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET"); // 逆ジオコーディングは通常GETリクエスト
        connection.setDoOutput(false);      // GETリクエストなので出力は不要

        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
                StringBuilder responseContent = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    responseContent.append(line);
                }
                return responseContent.toString();
            }
        } else {
            // エラーレスポンスの読み取り
            try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
                StringBuilder errorContent = new StringBuilder();
                String line;
                while ((line = errorReader.readLine()) != null) {
                    errorContent.append(line);
                }
                throw new RuntimeException("APIリクエスト失敗: HTTPステータス " + responseCode + ", レスポンス: " + errorContent.toString());
            }
        }
    }
}

出力例

上記コードで示された経緯度を使用した場合の、逆ジオコーディングによる住所情報の取得結果の例です。

取得した座標:
経度: 118.32739780938687
緯度: 35.081301798659695

逆ジオコーディング結果例:
{
  "status": "1",
  "info": "OK",
  "infocode": "10000",
  "regeocode": {
    "formatted_address": "山東省臨沂市蘭山区蘭山街道通達路銀座ショッピングモール(通達店)",
    "addressComponent": {
      "citycode": "0539",
      "province": "山東省",
      "adcode": "371302",
      "district": "蘭山区",
      "towncode": "371302001000",
      "streetNumber": {
        "number": "38号",
        "direction": "東北",
        "distance": "9.78993",
        "street": "通達路",
        "location": "118.327442,35.081382"
      },
      "country": "中国",
      "township": "蘭山街道",
      "city": "臨沂市",
      "businessAreas": [],
      "neighborhood": {
        "name": [],
        "type": []
      },
      "building": {
        "name": [],
        "type": []
      }
    }
  }
}

タグ: vue.js Geolocation API 高徳地図 座標変換 GCJ02

9月29日 21:13 投稿