Select to view content in your preferred language

MapLibre GL JS からプレイス サービスを使ってみる

825
0
10-21-2024 07:36 PM
Labels (2)

MapLibre GL JS からプレイス サービスを使ってみる

はじめに

オープンソースの MapLibre GL JS から ArcGIS Location Platform が提供するプレイス サービスを使ってみましたので、MapLibre GL JS によるプレイス サービスを使用した開発について簡単に紹介したいと思います。

ArcGIS Location Platform は、マッピングと空間解析のアプリケーションを構築する開発者向けの PaaSPlatform as a Service)サービスです。マッピング API SDK などを使用して、高品質のロケーション サービスにアクセスし、アプリケーションやビジネス システムにロケーション インテリジェンスを組み込むことができます。

プレイス サービスは、ArcGIS Location Platform が提供するロケーション サービスのひとつで、1,000 以上の主要地(POI)データのカテゴリーにアクセスできます。例えば、近傍検索では、ある位置から指定された半径内のプレイスを見つけたり、境界ボックス内のプレイス検索では、現在のマップ範囲内になるプレイスを検索して表示したり、各プレイスに関する詳細情報を取得できます。プレイス サービスについては、ArcGIS ブログ(英語)「Bring Places Data into Your Apps with the New ArcGIS Platform Places Service」、「Elevating Developer Experience: Fresh POI Data, Stylish Map Icons, and Localized Categories via ArcG...」でも紹介されていますので、ご確認ください。

プレイス サービスを使用するにあたり、開発を始めるためのガイドについては、ArcGIS Location Platform Place finding をご参照ください。プレイス サービスの仕様や使用方法、各 APISDK 等で使用する方法まで詳しく紹介されています。

今回は、MapLibre GL JS からプレイス サービスを使用し、Esri Developer のチュートリアル「Find nearby places and details」を参考にしています。Esri Developer には、ロケーション サービスの概要や使用方法、また、各 APISDK によるドキュメントが充実しています。

Esri Developer の概要については、ArcGIS ブログ(英語)「A new developer website experience」でも紹介していますので、ご確認ください。

2024-10-04_13h40_33.png

開発準備

プレイス サービスを使用した開発を始めるため、開発者アカウントを作成する必要があります。ArcGIS Location Platform が提供するロケーション サービスは、開発者アカウントを作成することで、無償から使用することができます。プレイス サービスも無料枠(毎月)があるため、手軽に使用することができます。

開発者アカウントの作成方法については、ArcGIS Developers 開発リソース集の開発者アカウントの作成をご確認ください。

そして、プレイス サービスにアクセスするため、API キーを使用します。API キーは、ArcGIS の安全なサービス、コンテンツ、および機能へのアクセスを許可する、長期間のアクセス トークンです。API キーの取得方法については、ArcGIS Developers 開発リソース集の API キーの取得をご確認ください。API キーの作成では、権限の選択で、アプリケーションが使用するコンテンツやサービスにアクセスするための適切な権限の設定が必要となります。今回はベースマップとプレイスを使用するための権限を付与します。これで、開発を始めるための準備は完了です。

MapLibre GL JS による開発

今回、MapLibre GL JS からプレイス サービスを使用した開発には、MapLibre GL JS のドキュメント「Find nearby places and details」を参考にしています。近傍検索をプレイス サービスを使用して行い、ある地点から指定された半径内にある POI データを検索します。

ここでは、注目したいポイントに絞ってコードを紹介しています。コードの詳細については、「Find nearby places and details」でも紹介していますので、合わせてご確認ください。

<! -- MapLibre GL JS の読み込み-->
<script src=https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.js></script>
<link href=https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.css rel="stylesheet" />

<!-- Calcite Components の読み込み -->
<script type="module" src="https://js.arcgis.com/calcite-components/2.12.1/calcite.esm.js"></script>
<link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.12.1/calcite.css" />

<!-- ArcGIS REST JS : リクエストとプレイスの読み込み -->
<script src="https://unpkg.com/@esri/arcgis-rest-request@4.0.0/dist/bundled/request.umd.js"></script>
<script src="https://unpkg.com/@esri/arcgis-rest-places@1.0.0/dist/bundled/places.umd.js"></script>

<!-- 検索範囲を視覚化するために使用するTurf.jsの読み込み -->
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>

 背景地図や地物などの地図表示には、MapLibre GL JS を使用します。近傍検索の結果の表示には、Calcite Components を使用してします。Calcite コンポーネントは、優れた Web エクスペリエンスを構築するための、構成可能な Web コンポーネントが用意されています。これにより、リッチな Web アプリを構築することが可能です。近傍検索の実行には、ArcGIS REST JSplaces パッケージを使用しています。ArcGIS REST JS は、ArcGIS のサービスにアクセスすることができ、マッピングや空間解析のアプリケーションを構築するための軽量な JavaScript のモジュールです。
その他、検索範囲を地図上で表示するため も使用しています。Turf.js は、オープンソースの地理空間解析ライブラリです。

// 検索カテゴリーの設定
let activeCategory = "16000";
let userLocation, clickedPoint;

// activeCategory を更新するイベント ハンドラー
categorySelect.addEventListener("calciteComboboxChange", e => {
activeCategory = categorySelect.value;
});

プレイス の検索を実行するには、カテゴリー IDが必要です。各カテゴリー IDは、「中華レストラン」(3099)や「スポーツ・レクリエーション」(18000)のように、プレイスはユニークなカテゴリーに対応しています。

使用可能なプレイスのカテゴリーの詳細については、「Find place categories」をご参照ください。

アクティブなプレイスのカテゴリーとクリックされた場所を追跡するグローバル変数を宣言します。アクティブなカテゴリーを 16000 に設定し、これはプレイスのランドマークとアウトドアのカテゴリーに対応しています。新しいカテゴリーがドロップダウンから選択されたときに activeCategory を更新するイベント ハンドラーを作成します。

const searchRadius = 750;

// クリックした地点と検索するエリアを表すレイヤーの設定
map.once("load", () => {
  map.addSource("clicked-point", {
    type: "geojson",
    data: {
      "type": "Feature",
      "geometry": {
         "type":"Point",
         "coordinates": []
      }
    }
  );
  map.addLayer({
    id:"search-location",
    source:'clicked-point',
    type:"fill",
    paint: {
        'fill-color': '#aaaaaa',
        'fill-opacity':0.25,
        'fill-outline-color':'#000000',
    }
  })
})

// 検索範囲の設定
map.on('click', function (e) {
  userLocation = e.lngLat;
 // turf.circle を使用して、ポイントの周囲にメートル単位の半径の円を作成
  const searchArea = turf.circle([e.lngLat.lng, e.lngLat.lat], searchRadius, {
    steps:36,
    units:"meters"
   });
   map.getSource('clicked-point').setData(searchArea);
})

 近傍検索は、ユーザーが指定した位置と半径を定義し、指定された位置から半径内のあるプレイス データを検索します。レイヤーの定義として、ユーザーが地図上でクリックした地点と検索範囲を表すレイヤーを設定します。クリック ハンドラーを追加して、ユーザーが地図上でクリックした地点の周囲にメートル単位の半径の円を表します。検索範囲を視覚化するために Turf.js を使用します。クリックが指定されたら、turf.circle を使って、ポイントの周囲にメートル単位の半径の円を作成します。 

function showPlaces() {
	// ArcGIS REST JS によるプレイスサービスへのリクエスト
	arcgisRest.findPlacesNearPoint({
	    x: userLocation.lng,
	    y: userLocation.lat,
	    categoryIds:activeCategory,
	    radius:searchRadius,
	    authentication
	})
};

位置とカテゴリーが設定されたら指定した地点の近くの場所を検索するため、プレイス サービスにリクエストします。新しい関数 showPlaces を定義します。内部では、ArcGIS REST JS findPlacesNearPoint 関数を使用して、Places サービスにリクエストを送信します。パラメーターには、userLocationsearchRadiusactiveCategoryauthentication オブジェクトを指定します。

// 結果の表示
function addResult(place) {
	// MapLibre の Marker による地図表示
	const marker = new maplibregl.Marker()
	    .setLngLat([place.location.x, place.location.y])
	    .addTo(map);
	currentPlaces.push(marker);

	// calcite-list-item 要素を使用して表示
	const infoDiv = document.createElement("calcite-list-item");
	resultPanel.appendChild(infoDiv);

	const description = `
	    ${place.categories[0].label} -
	    ${Number((place.distance / 1000).toFixed(1))} km
	`;

	infoDiv.label = place.name;
	infoDiv.description = description;

	// HTML 要素をクリックし、関連する Marker に対してポップアップ表示
	marker.id = place.placeId;
	marker.setPopup(new maplibregl.Popup().setText(place.name));
	infoDiv.addEventListener("click",e => {
	    marker.togglePopup();
	    // プレイスの詳細表示
	    getDetails(marker);

	})
};

プレイス サービスによって指定されたカテゴリーに一致する地点近くのプレイスのリストが返却されますので、結果を地図上にポイントとして表示します。地図上でのポイント表示には、MapLibre Marker クラスを使用します。また、各プレイスの情報を UI 上で表示するために、Calcite Design System Components を使用して表示します。プレイスの情報を表示するために、calcite-list-item 要素を作成します。アイテムのプロパティを設定して、プレイス名、カテゴリー、およびユーザーがクリックした地点からの距離を表示します。この要素を結果リストに追加します。

// プレイスの詳細の取得と表示
function getDetails(marker) {

	// ArcGIS REST JS による特定 POI に関する詳細情報の取得
	arcgisRest.getPlaceDetails({
	    placeId: marker.id,
	    requestedFields: ["all"],
	    authentication
	})
	
	.then((result)=>{

	    map.flyTo({center:marker.getLngLat()});

	    // calcite-flow-item 要素を使用して作成
	    infoPanel = document.createElement("calcite-flow-item");

	    // calcite-flow-item 要素のテンプレートに含まれる <flow> 要素にアイテムを追加
	    flow.appendChild(infoPanel);
	    const placeDetails = result.placeDetails;
	    infoPanel.heading = placeDetails.name;
	    infoPanel.description = placeDetails.categories[0].label;
	    
	    // setAttribute ヘルパー関数を呼び出して、属性が有効な場合に表示します。各属性のCalcite UIアイコンを選択します。
	    setAttribute("Description", "information", placeDetails?.description);
	    setAttribute("Address", "map-pin", placeDetails?.address?.streetAddress);
	    setAttribute("Phone", "mobile", placeDetails?.contactInfo?.telephone);
	    setAttribute("Hours", "clock", placeDetails?.hours?.openingText);
	    setAttribute("Rating", "star", placeDetails?.rating?.user);
	    setAttribute("Email", "email-address", placeDetails?.contactInfo?.email);
	    setAttribute("Website", "information", placeDetails?.contactInfo?.website?.split("://")[1].split("/")[0]);
	    setAttribute("Facebook", "speech-bubble-social", (placeDetails?.socialMedia?.facebookId) ? `www.facebook.com/${placeDetails.socialMedia.facebookId}` : null);
	    setAttribute("Twitter", "speech-bubbles", (placeDetails?.socialMedia?.twitter) ? `www.twitter.com/${placeDetails.socialMedia.twitter}` : null);
	    setAttribute("Instagram", "camera", (placeDetails?.socialMedia?.instagram) ? `www.instagram.com/${placeDetails.socialMedia.instagram}` : null);

	    infoPanel.addEventListener("calciteFlowItemBack", e => {
	        marker.togglePopup();
	    })

	});
}

// setAttribute ヘルパー関数
const setAttribute = (heading, icon, validValue) => {
  // 属性情報が有効な場合に表示
  if (validValue) {
    const element = document.createElement("calcite-block");
    element.heading = heading;
    element.description = validValue;
    // 各属性の Calcite UI アイコンを選択
    const attributeIcon = document.createElement("calcite-icon");
    attributeIcon.icon = icon;
    attributeIcon.slot = "icon";
    attributeIcon.scale = "m";

    element.appendChild(attributeIcon);
    infoPanel.appendChild(element);
  }
};

ユーザーがプレイスのリストからパネル内のプレイスをクリックして、詳細情報を表示します。特定のプレイスの詳細情報を取得するため ArcGIS REST JS getPlaceDetails 関数を使用します。リクエスト パラメーターには、プレイス ID、プレイスに対して返却される属性を定義するフィールドの配列、authentication オブジェクトを指定します。

返却された結果は、Calcite Design System Components calcite-flow-item 要素を使用します。calcite-flow-item 要素のテンプレートに含まれる <flow> 要素にアイテムを追加します。例えば、infoPanel.heading = placeDetails.name; では、heading にプレイスの名前を追加しています。

また、その他の詳細情報の表示には、setAttribute ヘルパー関数を呼び出して、属性が有効な場合に表示します。各属性の Calcite UI icon を選択して表示するように指定しています。

今回作成した全体のコードは以下となります。

<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <!-- MapLibre GL JS の読み込み -->
    <script src=https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.js></script>
    <link href=https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.css rel="stylesheet" />

    <!-- Calcite Components の読み込み -->
    <script type="module" src="https://js.arcgis.com/calcite-components/2.12.1/calcite.esm.js"></script>
    <link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.12.1/calcite.css" />

    <!-- ArcGIS REST JS: : リクエストとプレイスの読み込み -->
    <script src="https://unpkg.com/@esri/arcgis-rest-request@4.0.0/dist/bundled/request.umd.js"></script>
    <script src="https://unpkg.com/@esri/arcgis-rest-places@1.0.0/dist/bundled/places.umd.js"></script>

    <!--検索範囲を視覚化するために使用する Turf.js の読み込み -->
    <script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>

    <style>
      body {
          margin: 0;
          padding: 0;
          width:100%;
          height:100%;
          display:flex;
          flex-direction:row;
      }

      #map {
          position:absolute;
          left:350px;
          top:0;
          bottom:0;
          right:0;
          font-family: Arial, Helvetica, sans-serif;
          font-size: 14px;
          color: #323232;
          z-index: 1;
      }

      .contents {
        position:absolute;
        top:44px;
        bottom:0;
        left:0;
        width:350px;
        overflow-y: auto;
        overflow-x: hidden;
      }
      #categorySelect {
        margin: 5px;
        width:340px;
      }

  </style>
</head>
<body>

  <calcite-combobox id="categorySelect" placeholder="Filter by category" overlay-positioning="fixed" selection-mode="single">
    <calcite-combobox-item value="10000" text-label="アート/エンターテイメント"></calcite-combobox-item>
    <calcite-combobox-item value="11000" text-label="ビジネス/プロフェッショナル サービス"></calcite-combobox-item>
    <calcite-combobox-item value="12000" text-label="コミュニティ/行政機関"></calcite-combobox-item>
    <calcite-combobox-item value="13000" text-label="飲食店"></calcite-combobox-item>
    <calcite-combobox-item value="15000" text-label="健康/医療"></calcite-combobox-item>
    <calcite-combobox-item selected value="16000" text-label="ランドマーク/アウトドア"></calcite-combobox-item>
    <calcite-combobox-item value="17000" text-label="小売"></calcite-combobox-item>
    <calcite-combobox-item value="18000" text-label="スポーツ/レクリエーション"></calcite-combobox-item>
    <calcite-combobox-item value="19000" text-label="旅行/交通"></calcite-combobox-item>
  </calcite-combobox>

  <div class="contents">
    <calcite-flow id="flow">
      <calcite-flow-item>
        <calcite-list id="results">
          <calcite-notice open><div slot="message">地図上をクリックすると、その場所の周辺を検索することができます。</div></calcite-notice>
        </calcite-list>
      </calcite-flow-item>
    </calcite-flow>
  </div>

  <div id="map"></div>
  <script>

    const categorySelect = document.getElementById("categorySelect");
    const resultPanel = document.getElementById("results");
    const flow = document.getElementById("flow");
    let infoPanel;

    // setAttribute ヘルパー関数
    const setAttribute = (heading, icon, validValue) => {
        // 属性情報が有効な場合に表示
        if (validValue) {
            const element = document.createElement("calcite-block");
            element.heading = heading;
            element.description = validValue;
            // 各属性の Calcite UI アイコンを選択
            const attributeIcon = document.createElement("calcite-icon");
            attributeIcon.icon = icon;
            attributeIcon.slot = "icon";
            attributeIcon.scale = "m";

            element.appendChild(attributeIcon);
            infoPanel.appendChild(element);
        }
    };
    const accessToken = "<API Key>";
    
    const basemapEnum = "arcgis/streets";

    const map = new maplibregl.Map({
            container: "map", // the id of the div element
            style: `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapEnum}?token=${accessToken}&language=ja`,
            zoom: 14, // starting zoom
            center: [139.767125, 35.681236], // starting location [longitude, latitude]
            pitch: 50,
            hash: true
    });

    const authentication = arcgisRest.ApiKeyManager.fromKey(accessToken);

    // ズーム・回転
    map.addControl(
        new maplibregl.NavigationControl({
            visualizePitch: true,
            showZoom: true,
            showCompass: true
        })
    );
      
    // スケール表示
    map.addControl(new maplibregl.ScaleControl({
        maxWidth: 200,
        unit: 'metric'
    }));

    // 検索カテゴリーの設定
    let activeCategory = "16000";
    let userLocation, clickedPoint;

    const searchRadius = 150;

    // クリックした地点と検索するエリアを表すレイヤーの設定
    map.once("load", () => {

        map.addSource("clicked-point", {
            type: "geojson",
            data: {
            "type": "Feature",
            "geometry": {
                "type":"Point",
                "coordinates": []
            }}
        });
        map.addLayer({
            id:"search-location",
            source:'clicked-point',
            type:"fill",
            paint: {
                'fill-color': '#aaaaaa',
                'fill-opacity':0.25,
                'fill-outline-color':'#000000',
            }
        })

        map.addSource("start", {
          type: "geojson",
          data: {
            type: "FeatureCollection",
            features: []
          }
        });

        map.addLayer({
          id: "start-circle",
          type: "circle",
          source: "start",

          paint: {
            "circle-radius": 6,
            "circle-color": "white",
            "circle-stroke-color": "black",
            "circle-stroke-width": 2
          }
        });

        map.addSource("trailheads", {
          type: "geojson",
          data: `https://services.arcgis.com/wlVTGRSYTzAbjjiC/ArcGIS/rest/services/RealEstatePropertyMaster/FeatureServer/0/query?token=${accessToken}&f=pgeojson&where=1=1`,

        });

        map.addLayer({
          id: "trailheads-circle",
          type: "circle",
          source: "trailheads",

          paint: {
            "circle-color": "hsla(0,0%,0%,0.75)",
            "circle-stroke-width": 1.5,
            "circle-stroke-color": "white",
          }
        });
    })

    // 検索範囲の設定
    map.on('click', function (e) {

        const coordinates = e.lngLat.toArray();
        const point = {
          type: "Point",
          coordinates
        };
        map.getSource("start").setData(point);

        userLocation = e.lngLat;

        // turf.circle を使用して、ポイントの周囲にメートル単位の半径の円を作成
        const searchArea = turf.circle([e.lngLat.lng, e.lngLat.lat], searchRadius, {
            steps:36,
            units:"meters"
        });
        map.getSource('clicked-point').setData(searchArea);

        showPlaces();

    })

    // activeCategory を更新するイベント ハンドラ
    categorySelect.addEventListener("calciteComboboxChange", e => {
        activeCategory = categorySelect.value;

        if (userLocation) showPlaces();

    });

    const currentPlaces = [];

    function showPlaces() {

        for (let place of currentPlaces) {
                place.remove();
        }
        resultPanel.innerHTML = "";

        if (infoPanel) infoPanel.remove();
        
        // ArcGIS REST JS によるプレイスサービスへのリクエスト
        arcgisRest.findPlacesNearPoint({
            x: userLocation.lng,
            y: userLocation.lat,
            categoryIds:activeCategory,
            radius:searchRadius,
            authentication
        })
        .then((response)=>{
            response.results.forEach((result)=>{
                addResult(result);
            });
        });
    };

    // 結果の表示
    function addResult(place) {
        // MapLibre の Marker による地図表示
        const marker = new maplibregl.Marker()
            .setLngLat([place.location.x, place.location.y])
            .addTo(map);
        currentPlaces.push(marker);

        // calcite-list-item 要素を使用して表示
        const infoDiv = document.createElement("calcite-list-item");
        resultPanel.appendChild(infoDiv);

        const description = `
            ${place.categories[0].label} -
            ${Number((place.distance / 1000).toFixed(1))} km
        `;

        infoDiv.label = place.name;
        infoDiv.description = description;
        
        // HTML 要素をクリックし、関連する Marker に対してポップアップ表示
        marker.id = place.placeId;
        marker.setPopup(new maplibregl.Popup().setText(place.name));
        infoDiv.addEventListener("click",e => {
            marker.togglePopup();
            // プレイスの詳細表示
            getDetails(marker);
        })
    }

    // プレイスの詳細の取得と表示
    function getDetails(marker) {
        // ArcGIS REST JS による特定 POI に関する詳細情報の取得
        arcgisRest.getPlaceDetails({
            placeId: marker.id,
            requestedFields: ["all"],
            authentication
        })

        .then((result)=>{

            map.flyTo({center:marker.getLngLat()});

            // calcite-flow-item 要素を使用して作成
            infoPanel = document.createElement("calcite-flow-item");

            // calcite-flow-item 要素のテンプレートに含まれる <flow> 要素にアイテムを追加
            flow.appendChild(infoPanel);
            const placeDetails = result.placeDetails;
            infoPanel.heading = placeDetails.name;
            infoPanel.description = placeDetails.categories[0].label;
            
            // setAttribute ヘルパー関数を呼び出して、属性が有効な場合に表示します。各属性の Calcite UI アイコンを選択します。
            setAttribute("Description", "information", placeDetails?.description);
            setAttribute("Address", "map-pin", placeDetails?.address?.streetAddress);
            setAttribute("Phone", "mobile", placeDetails?.contactInfo?.telephone);
            setAttribute("Hours", "clock", placeDetails?.hours?.openingText);
            setAttribute("Rating", "star", placeDetails?.rating?.user);
            setAttribute("Email", "email-address", placeDetails?.contactInfo?.email);
            setAttribute("Website", "information", placeDetails?.contactInfo?.website?.split("://")[1].split("/")[0]);
            setAttribute("Facebook", "speech-bubble-social", (placeDetails?.socialMedia?.facebookId) ? `www.facebook.com/${placeDetails.socialMedia.facebookId}` : null);
            setAttribute("Twitter", "speech-bubbles", (placeDetails?.socialMedia?.twitter) ? `www.twitter.com/${placeDetails.socialMedia.twitter}` : null);
            setAttribute("Instagram", "camera", (placeDetails?.socialMedia?.instagram) ? `www.instagram.com/${placeDetails.socialMedia.instagram}` : null);

            infoPanel.addEventListener("calciteFlowItemBack", e => {
                marker.togglePopup();
            })
        });
    }
    </script>
</body>
</html>

すぐに動作を確認されたい方は、Esri がサンプルとして でも公開していますので、ご確認ください。ただし、認証の設定が必要となりますので、API キーを取得して指定する必要がございます。

今回紹介した近傍検索については、Mapping and location services guide Nearby Search にて具体的な使い方なども紹介していますので、ご確認ください。

まとめ

オープンソースの MapLibre GL JS からプレイス サービスを使用した方法を中心に紹介しました。ArcGIS Location Platform が提供するロケーション サービスとして、プレイス サービスについても紹介しました。

MapLibre GL JS からプレイス サービスの使用方法については、Find places in a bounding box でも紹介しています。こちらでは、検索ボックスからテキスト ベースで内容を入力して検索を実行し、バウンディング ボックス内のプレイスを検索する方法を学ぶことができます。こちらもご確認ください。

MapLibre GL JS やプレイス サービスについて、ぜひ、この機会にお試しください!

 

■参考サイト

Labels (2)
Version history
Last update:
‎10-21-2024 07:36 PM
Updated by:
Contributors