OpenLayers を活用して OSM/WMS/WFS レイヤを Web ブラウザ上に描画する方法

WMS や WFS といったサービスを利用して地理情報を取得し、Web アプリケーションとして描画することができます。この記事では、JavaScript ライブラリである OpenLayers を使用して、様々なレイヤ（OSM、WMS、WFS）を読み込みおよび描画をし、操作する手順について説明していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/3c6de0b0-f3c9-b37e-6338-56017d538418.png)

OpenLayers は、ウェブブラウザ上で地図を表示し、様々なデータソースから地図データを取得して操作するためのオープンソースの JavaScript ライブラリです。このライブラリは、地理的情報を扱うウェブアプリケーションを開発するために広く利用されており、Google Maps API や Bing Maps API といった他の商用地図サービスとも組み合わせて使用できます。OpenLayers を使用することで、ユーザは地図上にオーバーレイとして画像やベクターデータを表示したり、地図をパンやズームするなどのインタラクティブな操作が可能になります。

# 1. 事前準備

地図を表示するための要素を作成します。`id` 属性が `"map"` の要素が、今回地図を表示する領域となります。
```html
<div id="map"></div>
```

CSS スタイルで、地図の表示領域 (`#map`) の幅と高さを指定しておきます。
```html
<style>
    #map {
        width: 100%;
        height: 500px;
    }
</style>
```

# 2. OSM (OpenStreetMap) の読み込みと描画の詳細
OSM は一般公開されている地図データベースであり、Web アプリケーション等で広く使われています。
また、今回は OpenLayers を使って OSM を含む地図の表示などを行っていきます。

## ステップ 1: OpenLayers のセットアップ

`<head>` セクションで、OpenLayers のスタイルシートを読み込むようにします。
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/ol.css" type="text/css">
```

加えて、`<body>` セクションで、OpenLayers の JavaScript を読み込みます。
```html
<script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
```

## ステップ 2: OSM レイヤを作成
まず、`ol.layer.Tile` オブジェクトを作成します。これは地図のタイルを表示するためのレイヤです。タイルは地図を構成する小さな画像の断片で、必要に応じてサーバーから読み込まれます。
`new ol.source.OSM()` で、OpenStreetMap から直接タイルをロードします。

```javascript
var osmLayer = new ol.layer.Tile({
    source: new ol.source.OSM()  // OpenStreetMap のソースを使用
});
```

## ステップ 3: 地図オブジェクトにレイヤを追加
次に、地図オブジェクトを初期化し、上で作成した OSM レイヤを地図に追加します。地図オブジェクトは特定の HTML 要素（ここでは `id` が 'map' の `div` 要素）内に描画されます。

```javascript
var map = new ol.Map({
    target: 'map',  // マップを表示する要素の ID
    layers: [osmLayer],  // マップに追加するレイヤ
    view: new ol.View({
        center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
        zoom: 10  // 初期のズームレベル
    })
});
```
`target` は地図を表示する HTML 要素の ID です。`layers` には表示するレイヤのリストを配列として渡します。この例では OSM のレイヤのみを渡しています。`ol.View` は地図のビューを設定します。ここでは地図の中心点とズームレベルを指定しています。中心点の指定では、`ol.proj.fromLonLat` で、経度と緯度をメルカトル座標系（EPSG:3857）に変換しています。

:::note info
ここまでのソースコードは、[参考: ソースコード](#参考-ソースコード)の**2. OSM (OpenStreetMap) の読み込みと描画の詳細**に掲載しています。
:::

# 3. WMS (Web Map Service) レイヤの読み込みと描画の詳細

WMS は、地図サービスの一種であり、地理空間プロセスの結果を画像として提供するサービスです。このセクションでは、WMS レイヤを読み込み、それを地図上に表示する方法を解説します。

## ステップ 1: WMS レイヤの設定
WMS レイヤを設定するには、`ol.layer.Tile` を使用し、そのソースとして `ol.source.TileWMS` のインスタンスを設定します。ここでは、リクエストの URL や WMS サービスの詳細を指定します。

```javascript
var wmsLayer = new ol.layer.Tile({
    source: new ol.source.TileWMS({
        url: 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?REQUEST=WMS&MAP=C:/OSGeo4W/apps/qgis/test.qgz&LAYERS=Sample_Ortho_30cm_8bit_3band_20190525_25',
        serverType: 'qgis'  // サーバータイプを指定
    })
});
```
ここで指定された URL は、WMS サービスのエンドポイントです。これにより、指定されたパラメータに基づいて地図の画像が生成され、それがレイヤとして表示されます。今回は、「[QGIS での WMS/WFS サーバ構築方法](https://qiita.com/haystacker/private/611bb740744a4eb876e7)」で設定した WMS サーバを設定しています。

`ol.source.TileWMS` の `serverType` は、OpenLayers が WMS (Web Map Service) サーバーからタイルを取得する際に、特定のサーバータイプに適応する特定の最適化やパラメータ調整を行うために使用されます。

| `serverType` 値 | 説明 |
|:--|:--|
| `mapserver` | **MapServer** はオープンソースの地図サーバーソフトウェアで、高性能な地図画像生成とデータ配信が可能 |
| `geoserver` | **GeoServer** はオープンソースの地理情報システム (GIS) サーバーで、ユーザフレンドリーな管理インターフェースが特徴 |
| `carmentaserver` | **Carmenta Server** は商用 GIS サーバーソフトウェアで、高度なセキュリティと信頼性が要求される企業向けに設計されている |
| `qgis` | **QGIS Server** は、デスクトップ GIS アプリケーションである QGIS のサーバーコンポーネント |

## ステップ 2: 地図に WMS レイヤを追加
前述の地図オブジェクトに WMS レイヤ (`wmsLayer`) を追加することで、地図上に WMS サービスから提供される画像を描画できます。

```diff_javascript
 var map = new ol.Map({
     target: 'map',  // マップを表示する要素の ID
-    layers: [osmLayer],  // マップに追加するレイヤ
+    layers: [osmLayer, wmsLayer],  // マップに追加するレイヤ
     view: new ol.View({
         center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
         zoom: 10  // 初期のズームレベル
     })
 });
```

これで、地図上に WMS レイヤが表示され、ユーザはWMSサービスから提供される地理情報画像を見ることができます。

:::note info
ここまでのソースコードは、[参考: ソースコード](#参考-ソースコード)の**3. WMS (Web Map Service) レイヤの読み込みと描画の詳細**に掲載しています。
:::

# 4. WFS (Web Feature Service) レイヤの読み込みと描画の詳細

WFS は、地理空間データをベクター形式で提供するサービスです。このセクションでは、WFS レイヤを読み込み、それを地図上に表示する方法について解説します。

## ステップ 1: ベクターソースの設定
WFS レイヤを読み込むためには、`ol.source.Vector` を使用してデータソースを設定し、`loader` 関数を定義して特定の範囲のデータを動的にリクエストします。これは、地図の表示領域に基づいてデータをロードすることで効率的なデータ処理を可能にします。
URL には、「[QGIS での WMS/WFS サーバ構築方法](https://qiita.com/haystacker/private/611bb740744a4eb876e7)」で設定している WFS サーバを設定しています。

```javascript
var vectorSource = new ol.source.Vector({
    loader: function(extent, resolution, projection) {
        // WFS サービスからデータを読み込むための URL を構築
        var url = 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?' +
            'MAP=C:/OSGeo4W/apps/qgis/test2.qgz&' +
            'SERVICE=WFS&REQUEST=GetFeature&' +
            'VERSION=1.1.0&TYPENAME=W05-08_13-g_Stream&' +
            'SRSNAME=EPSG:3857&' +
            'BBOX=' + extent.join(',') + ',EPSG:3857&' +
            'outputFormat=text/xml; subtype=gml/3.1.1';
        fetch(url).then(function(response) {
            return response.text();  // レスポンスをテキストとして取得
        }).then(function(text) {
            // 取得した WFS データをフィーチャーとして読み込み
            var features = new ol.format.WFS().readFeatures(text, {
                dataProjection: 'EPSG:3857',                      // データの投影法
                featureProjection: map.getView().getProjection()  // マップの投影法
            });
            vectorSource.addFeatures(features);  // フィーチャーをソースに追加
        }).catch(function(error) {
            console.error('Error loading WFS: ', error);  // エラー処理
        });
    },
    strategy: ol.loadingstrategy.bbox  // ローディング戦略（ここでは bbox を使用）
});
```
このコードにより、地図の表示範囲が変わるたびに、新たな範囲に応じたデータがサーバーから取得されます。`fetch` 関数を使って WFS サービスからデータを非同期に取得し、取得したデータは`ol.format.WFS().readFeatures` を使用して OpenLayers が理解できる形式に変換されます。

## ステップ 2: WFS レイヤの設定
`ol.layer.Vector` を使用してレイヤを初期化し、先ほど設定したベクターソースをレイヤに設定します。

```javascript
var wfsLayer = new ol.layer.Vector({
    source: vectorSource,
});
```

## ステップ 3: 地図に WFS レイヤを追加
前述の地図オブジェクトに WFS レイヤ (`wfsLayer`) を追加することで、地図上に WFS サービスから提供される地図データを描画できます。

```diff_javascript
 var map = new ol.Map({
     target: 'map',  // マップを表示する要素の ID
-    layers: [osmLayer, wmsLayer],  // マップに追加するレイヤ
+    layers: [osmLayer, wmsLayer, wfsLayer],  // マップに追加するレイヤ
     view: new ol.View({
         center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
         zoom: 10  // 初期のズームレベル
     })
 });
```

このレイヤは地図上にベクターデータを描画し、ユーザは WFS サービスから提供される詳細な地理情報を視覚化できます。`zIndex` はこのレイヤが他のレイヤよりも上に表示されることを意味します。

:::note info
ここまでのソースコードは、[参考: ソースコード](#参考-ソースコード)の**4. WFS (Web Feature Service) レイヤの読み込みと描画の詳細**に掲載しています。
:::

# 5. 主要機能の実装方法

Web ベースの地図アプリケーションにおいて、レイヤの動的管理はユーザ体験を向上させる重要な機能です。このセクションでは、レイヤの順序を変更する機能、ボタンの有効/無効化、および透過率の制御について具体的な実装方法を説明します。


## 5.1. レイヤの表示/非表示設定
ここでは、レイヤ（地図などの要素）の表示と非表示を制御する機能の実装について解説していきます。

HTMLで、レイヤの表示/非表示のためのコントロールを定義します。各レイヤにはチェックボックスがあり、それぞれのラベルにはレイヤ名が表示されます。

```html
<div id="layerControls">
    <!-- 各レイヤの制御パネル -->
    <div id="wfsControl">
        <!-- レイヤの表示・非表示を切り替えるチェックボックス -->
        <label>
            <input type="checkbox" id="wfsVisibility" checked> [WFS] W05-08_13-g_Stream
        </label>
    </div>
    <div id="wmsControl">
        <label>
            <input type="checkbox" id="wmsVisibility" checked> [WMS] Sample_Ortho_30cm_8bit_3band_20190525_25
        </label>
    </div>
    <div id="osmControl">
        <label>
            <input type="checkbox" id="osmVisibility" checked> [OSM] Base Layer
        </label>
    </div>
</div>
```

続けて、JavaScriptで、各チェックボックスの状態変化に対するリスナを定義します。
例えば、osmVisibilityチェックボックスの状態が変化したときには、osmLayerという変数に関連付けられたOpenStreetMapレイヤの表示状態が変更されます。

```javascript
document.getElementById('osmVisibility').addEventListener('change', function() {
    osmLayer.setVisible(this.checked);
});
```


これで、ユーザはウェブページ上で各レイヤの表示状態を切り替えることができ、地図や他の地理空間データの可視化を制御することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/8d8bfe0a-4c1b-48e8-dda0-ffd21d026a0d.png)

## 5.2. レイヤ透過率の設定

レイヤの透過率を調整することは、地図上で複数のレイヤを重ね合わせる際に特に有用です。

```diff_html
         <div id="osmControl">
             <label>
                 <input type="checkbox" id="osmVisibility" checked> [OSM] Base Layer
             </label>
+            <label class="slider-label_osm">
+                &emsp;&emsp;透過率:
+                <input type="range" id="osmOpacity" min="0" max="1" step="0.1" value="1">
+            </label>
         </div>
```

併せて、透過率を調整するスライダをブロック要素として表示するようにしておきます。

```css
.slider-label_osm, .slider-label_wms, .slider-label_wfs {
    display: block;
}
```

透過率を調整するスライダの値が変更されたときに、選択されたレイヤの `opacity` プロパティを更新するようにします。`parseFloat(this.value)` はスライダから数値を取得し、その値をレイヤの透過率として設定します。

```javascript
document.getElementById('osmOpacity').addEventListener('input', function() {
    osmLayer.setOpacity(parseFloat(this.value));
});
```

レイヤが非表示となっている時は、透過率を指定する必要はないので、透過率を調整スライダ非表示にするための関数を定義します。

```
function toggleOsmOpacityControl(visible) {
    var opacityControl = document.querySelector('.slider-label_osm');
    opacityControl.style.display = visible ? 'block' : 'none';  // visible が true なら表示、false なら非表示
}
```

先ほど作成した関数を、レイヤの表示/非表示が変わったタイミングで呼び出すようにします。
```diff_html
 document.getElementById('osmVisibility').addEventListener('change', function() {
     osmLayer.setVisible(this.checked);
+    toggleOsmOpacityControl(this.checked);
 });
 ```

これで、レイヤが表示されている際には、透過率調整を行うスライダが表示され、レイヤ透過率の変更ができるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/c3fd69d9-4c7c-1262-4d38-e9f1de0b14c7.png)

## 5.3. レイヤの順序変更

レイヤの順序を変更する機能は、レイヤの `zIndex` プロパティを動的に更新することで実現されます。以下に、レイヤを上に移動する関数の例を示します。


```diff_html
 <div id="osmControl">
+    <button type="button" id="osmUp">▲</button>
+    <button type="button" id="osmDown">▼</button>&ensp;
     <label>
         <input type="checkbox" id="osmVisibility" checked> [OSM] Base Layer
     </label>
     <label class="slider-label_osm">
         &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;透過率:
         <input type="range" id="osmOpacity" min="0" max="1" step="0.1" value="1">
     </label>
 </div>
```

```diff_html
 var osmLayer = new ol.layer.Tile({
     source: new ol.source.OSM(),  // OpenStreetMap のソースを使用
+    zIndex: 0,                    // レイヤの重なり順序（z-index）
 });
```

```javascript
function moveLayerDown(layer) {
    var layersCollection = map.getLayers();    // マップのレイヤ・コレクションを取得
    var layers = layersCollection.getArray();  // レイヤ・コレクションを配列として取得
    var index = layers.indexOf(layer);         // 移動対象のレイヤのインデックスを取得

    if (index > 0) {  // インデックスが0より大きい場合（最上層でない場合）
        var aboveLayer = layers[index - 1];        // 一つ上のレイヤを取得
        var currentZIndex = layer.getZIndex();     // 現在の z-index を取得
        var aboveZIndex = aboveLayer.getZIndex();  // 上のレイヤの z-index を取得

        // z-index を交換してレイヤの順番を変更
        layer.setZIndex(aboveZIndex);
        layersCollection.remove(layer);
        layersCollection.insertAt(index - 1, layer);
        aboveLayer.setZIndex(currentZIndex);

        // マップの再描画とDOMの更新
        map.render();
        updateDOMOrder();
        updateButtonStates();  // ボタンの状態を更新
    }
}
```

この関数は、指定されたレイヤを一つ上の位置に移動します。`zIndex` の値を交換し、OpenLayers の内部レイヤ配列を更新することで、レイヤの表示順序を変更します。この操作は、地図上のレイヤの視覚的な重なりを変更するために使用されます。


各ボタンにイベントリスナを設定
```javascript
document.getElementById('osmDown').addEventListener(
    'click',
    function() {
        moveLayerDown(osmLayer);
    }
);
document.getElementById('osmUp').addEventListener(
    'click',
    function() {
        moveLayerUp(osmLayer);
    }
);
```

これで、レイヤの描画順序を入れ替えることができるようになりました。しかし、このままでは、特定のレイヤが見えない状態のときに、上に他のレイヤがあって隠れているだけなのか、そもそも描画されていないのか、判断が難しい状態となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/e104ec2e-4f1b-0bb2-6791-776845c45664.png)

後続のセクションでは、これに対応するため、上部に表示しているレイヤ一覧の順番も入れ替えることで、直感的にレイヤ順序が把握できるようにしてきます。

## 5.4. レイヤ順序に応じたレイヤ一覧の表示入れ替え

ここでは、「▲」または「▼」を押下し、レイヤ順序を入れ替える際に、ページ上部に表示しているレイヤ一覧の表示順も入れ替えるようにしていきます。


各レイヤを ID で識別できるようにします。

```diff_javascript
 var osmLayer = new ol.layer.Tile({
     source: new ol.source.OSM(),  // OpenStreetMap のソースを使用
     zIndex: 0,                    // レイヤの重なり順序（z-index）
+    id: 'osm'                     // レイヤの識別 ID
 });
```

ID でレイヤを取得する関数を定義します。
```javascript
function getLayerById(id) {
    var layers = map.getLayers().getArray();
    return layers.find(layer => layer.get('id') === id);
}
```

HTML ドキュメント内の特定の要素（この場合は `#layerControls` 内の `div` 要素）の表示順序を更新するため、`updateDOMOrder` 関数を定義します。

```javascript
function updateDOMOrder() {
    var layerElems = Array.from(document.querySelectorAll('#layerControls > div'));
    layerElems.sort((a, b) => {
        var layerA = getLayerById(a.id.replace('Control', ''));
        var layerB = getLayerById(b.id.replace('Control', ''));
        return layerB.getZIndex() - layerA.getZIndex();
    });
    var parent = document.getElementById('layerControls');
    layerElems.forEach(elem => parent.appendChild(elem));
}
```
```html: 表示順序の入れ替え対象
    <div id="layerControls">
        <div id="wfsControl">
          <!-- 省略 -->
        </div>
        <div id="wmsControl">
          <!-- 省略 -->
        </div>
        <div id="osmControl">
          <!-- 省略 -->
        </div>
    </div>
```

この関数は、地理情報システム (GIS) やウェブマッピングアプリケーションでよく使われる処理です。レイヤのZインデックス（重なり順）に基づいて DOM 内でのレイヤコントロールの順序をソートし直すことにより、ユーザインターフェイスの整合性と視覚的表現の一貫性を保持します。具体的な処理は以下の通りです。

1. **要素の選択**
    - `document.querySelectorAll('#layerControls > div')` は、`id` が `layerControls` の要素の直接の子要素である `div` 要素すべてを選択します。これにより、レイヤごとのコントロールが含まれた div 要素が取得されます。

2. **配列への変換**
    - `Array.from()` は、選択された要素 (NodeList) を配列に変換します。これにより、配列のメソッド（例えば `sort`）を利用できるようになります。

3. **要素のソート**
    - `layerElems.sort()` は、配列内の要素をソートするメソッドです。ここでのソート処理では、各 `div` 要素の `id` 属性から `Control` という文字を削除して実際のレイヤの ID を取得し、`getLayerById` 関数を使用して対応するレイヤオブジェクトを取得します。
    - 各レイヤオブジェクトの `getZIndex()` メソッドを呼び出し、得られた Z インデックスに基づいて逆順にソートします（ Z インデックスが高い要素が先に来るようにします）。

4. **DOM の更新**
    - ソートされた `div` 要素を、`layerControls`という ID を持つ親要素に再び追加します。`appendChild` メソッドを使用すると、対象の要素がすでに DOM に存在する場合、その要素は現在の位置から新しい位置に移動します。これにより、`div` 要素の順序が更新され、表示が反映されます。

最後に、レイヤ順序を変更した際に `updateDOMOrder` 関数が呼び出されるように設定しておきます。
```diff_javascript
 function moveLayerDown(layer) {
     // 省略
     if (index > 0) {
         // 省略
         map.render();
+        updateDOMOrder();
     }
 }

 function moveLayerUp(layer) {
     // 省略
     if (index < layers.length - 1) {
         map.render();
+        updateDOMOrder();
     }
 }
 ```

これで、「▲」や「▼」ボタンを押下して、レイヤ順序を入れ替えた際に、上部のレイヤ一覧の並び順も変更されるようになり、より直感的になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/c7debac4-25d4-79d2-b3ce-65ecd94386d2.png)

## 5.5. ボタンの有効/無効化

ユーザがレイヤの順序を変更する際、最上層や最下層のレイヤでは特定のボタン（上に移動、下に移動）を無効化する必要があります。
以下の関数は各レイヤの位置を調べ、最上層のレイヤでは「上に移動」ボタンを無効にし、最下層のレイヤでは「下に移動」ボタンを無効にします。

```javascript
function updateButtonStates() {
    var layers = map.getLayers().getArray();  // マップの全レイヤを取得
    var layerCount = layers.length;  // レイヤの総数

    // 各レイヤに対してボタンの状態を更新
    layers.forEach((layer, index) => {
        var layerId = layer.get('id');  // レイヤの ID を取得
        var downButton = document.getElementById(layerId + 'Down');  // 下ボタンの要素を取得
        var upButton = document.getElementById(layerId + 'Up');  // 上ボタンの要素を取得

        // 最下層のレイヤの場合、下へのボタンを無効化
        if (index === 0) {
            downButton.disabled = true;
        } else {
            downButton.disabled = false;
        }

        // 最上層のレイヤの場合、上へのボタンを無効化
        if (index === layerCount - 1) {
            upButton.disabled = true;
        } else {
            upButton.disabled = false;
        }
    });
}
```

初期状態と、DOM の順序を入れ替えるのと同じタイミングで `updateButtonStates` 関数が呼び出されるように設定します。

```diff_html
 <script>
 document.addEventListener('DOMContentLoaded', function () {
     // 省略
+   updateButtonStates();
 });
 </script>
```

```diff_javascript
 function moveLayerDown(layer) {
     // 省略
     if (index > 0) {
         // 省略
         map.render();
         updateDOMOrder();
+        updateButtonStates();
     }
 }

 function moveLayerUp(layer) {
     // 省略
     if (index < layers.length - 1) {
         map.render();
         updateDOMOrder();
+        updateButtonStates();
     }
 }
 ```

これで、レイヤが最上部にあるときは「▲」ボタンを、最下部にあるときは「▼」ボタンを、無効にすることができるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/c249f80e-df3e-4f71-b43f-96c9756d6073.png)

:::note info
ここまでのソースコードは、[参考: ソースコード](#参考-ソースコード)の**ソースコード全文**に掲載しています。
:::

# 参考: ソースコード

<details>
  <summary>2. OSM (OpenStreetMap) の読み込みと描画の詳細</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OSM/WMS/WFS レイヤの描画</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/ol.css" type="text/css">
    <style>
        #map {
            width: 100%;
            height: 500px;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
    <script>
    // ページの読み込みが完了した後に実行される関数
    document.addEventListener('DOMContentLoaded', function () {
        // OpenLayers ライブラリを使って OSM のタイルレイヤを作成
        var osmLayer = new ol.layer.Tile({
            source: new ol.source.OSM()  // OpenStreetMap のソースを使用
        });

        // マップの設定
        var map = new ol.Map({
            target: 'map',  // マップを表示する要素の ID
            layers: [osmLayer],  // マップに追加するレイヤ
            view: new ol.View({
                center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
                zoom: 10  // 初期のズームレベル
            })
        });
    });
    </script>
</body>
</html>
```
</details>

<details>
  <summary>3. WMS (Web Map Service) レイヤの読み込みと描画の詳細</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OSM/WMS/WFS レイヤの描画</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/ol.css" type="text/css">
    <style>
        #map {
            width: 100%;
            height: 500px;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
    <script>
    // ページの読み込みが完了した後に実行される関数
    document.addEventListener('DOMContentLoaded', function () {
        // OpenLayers ライブラリを使って OSM のタイルレイヤを作成
        var osmLayer = new ol.layer.Tile({
            source: new ol.source.OSM()  // OpenStreetMap のソースを使用
        });

        // WMS レイヤの設定
        var wmsLayer = new ol.layer.Tile({
            source: new ol.source.TileWMS({
                url: 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?REQUEST=WMS&MAP=C:/OSGeo4W/apps/qgis/test.qgz&LAYERS=Sample_Ortho_30cm_8bit_3band_20190525_25',
                serverType: 'qgis'  // サーバータイプを指定
            })
        });

        // マップの設定
        var map = new ol.Map({
            target: 'map',  // マップを表示する要素の ID
            layers: [osmLayer, wmsLayer],  // マップに追加するレイヤ
            view: new ol.View({
                center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
                zoom: 10  // 初期のズームレベル
            })
        });
    });
    </script>
</body>
</html>
```
</details>

<details>
  <summary>4. WFS (Web Feature Service) レイヤの読み込みと描画の詳細</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OSM/WMS/WFS レイヤの描画</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/ol.css" type="text/css">
    <style>
        #map {
            width: 100%;
            height: 500px;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
    <script>
    // ページの読み込みが完了した後に実行される関数
    document.addEventListener('DOMContentLoaded', function () {
        // OpenLayers ライブラリを使って OSM のタイルレイヤを作成
        var osmLayer = new ol.layer.Tile({
            source: new ol.source.OSM()  // OpenStreetMap のソースを使用
        });

        // WMS レイヤの設定
        var wmsLayer = new ol.layer.Tile({
            source: new ol.source.TileWMS({
                url: 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?REQUEST=WMS&MAP=C:/OSGeo4W/apps/qgis/test.qgz&LAYERS=Sample_Ortho_30cm_8bit_3band_20190525_25',
                serverType: 'qgis'  // サーバータイプを指定
            })
        });

        // WFS データの読み込み設定
        var vectorSource = new ol.source.Vector({
            loader: function(extent, resolution, projection) {
                // WFS サービスからデータを読み込むための URL を構築
                var url = 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?' +
                    'MAP=C:/OSGeo4W/apps/qgis/test2.qgz&' +
                    'SERVICE=WFS&REQUEST=GetFeature&' +
                    'VERSION=1.1.0&TYPENAME=W05-08_13-g_Stream&' +
                    'SRSNAME=EPSG:3857&' +
                    'BBOX=' + extent.join(',') + ',EPSG:3857&' +
                    'outputFormat=text/xml; subtype=gml/3.1.1';
                fetch(url).then(function(response) {
                    return response.text();  // レスポンスをテキストとして取得
                }).then(function(text) {
                    // 取得した WFS データをフィーチャーとして読み込み
                    var features = new ol.format.WFS().readFeatures(text, {
                        dataProjection: 'EPSG:3857',                      // データの投影法
                        featureProjection: map.getView().getProjection()  // マップの投影法
                    });
                    vectorSource.addFeatures(features);  // フィーチャーをソースに追加
                }).catch(function(error) {
                    console.error('Error loading WFS: ', error);  // エラー処理
                });
            },
            strategy: ol.loadingstrategy.bbox  // ローディング戦略（ここでは bbox を使用）
        });

        // WFS レイヤの設定
        var wfsLayer = new ol.layer.Vector({
            source: vectorSource,
        });

        // マップの設定
        var map = new ol.Map({
            target: 'map',  // マップを表示する要素の ID
            layers: [osmLayer, wmsLayer, wfsLayer],  // マップに追加するレイヤ
            view: new ol.View({
                center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
                zoom: 10  // 初期のズームレベル
            })
        });
    });
    </script>
</body>
</html>
```
</details>

<details>
  <summary>ソースコード全文</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OSM/WMS/WFS レイヤの描画</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/ol.css" type="text/css">
    <style>
        #map {
            width: 100%;
            height: 500px;
        }
        .slider-label_osm, .slider-label_wms, .slider-label_wfs {
            display: block;
        }
    </style>
</head>
<body>
    <h2>OSM/WMS/WFS レイヤの描画</h2>
    <div id="layerControls">
        <!-- 各レイヤの制御パネル -->
        <div id="wfsControl">
            <!-- レイヤを上に移動するボタン -->
            <button type="button" id="wfsUp">▲</button>
            <!-- レイヤを下に移動するボタン -->
            <button type="button" id="wfsDown">▼</button>&ensp;
            
            <!-- レイヤの表示・非表示を切り替えるチェックボックス -->
            <label>
                <input type="checkbox" id="wfsVisibility" checked> [WFS] W05-08_13-g_Stream
            </label>
            
            <!-- 透過率の調整スライダ -->
            <label class="slider-label_wfs">
                &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;透過率:
                <input type="range" id="wfsOpacity" min="0" max="1" step="0.1" value="1">
            </label>
        </div>
        <div id="wmsControl">
            <button type="button" id="wmsUp">▲</button>
            <button type="button" id="wmsDown">▼</button>&ensp;
            <label>
                <input type="checkbox" id="wmsVisibility" checked> [WMS] Sample_Ortho_30cm_8bit_3band_20190525_25
            </label>
            <label class="slider-label_wms">
                &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;透過率:
                <input type="range" id="wmsOpacity" min="0" max="1" step="0.1" value="1">
            </label>
        </div>
        <div id="osmControl">
            <button type="button" id="osmUp">▲</button>
            <button type="button" id="osmDown">▼</button>&ensp;
            <label>
                <input type="checkbox" id="osmVisibility" checked> [OSM] Base Layer
            </label>
            <label class="slider-label_osm">
                &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&nbsp;透過率:
                <input type="range" id="osmOpacity" min="0" max="1" step="0.1" value="1">
            </label>
        </div>
    </div>
    <div id="map"></div>
    <script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
    <script>
    // ページの読み込みが完了した後に実行される関数
    document.addEventListener('DOMContentLoaded', function () {
        // OpenLayers ライブラリを使って OSM のタイルレイヤを作成
        var osmLayer = new ol.layer.Tile({
            source: new ol.source.OSM(),  // OpenStreetMap のソースを使用
            zIndex: 0,                    // レイヤの重なり順序（z-index）
            id: 'osm'                     // レイヤの識別 ID
        });

        // WMS レイヤの設定
        var wmsLayer = new ol.layer.Tile({
            source: new ol.source.TileWMS({
                url: 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?REQUEST=WMS&MAP=C:/OSGeo4W/apps/qgis/test.qgz&LAYERS=Sample_Ortho_30cm_8bit_3band_20190525_25',
                serverType: 'qgis'  // サーバータイプを指定
            }),
            zIndex: 1,  // z-index の設定
            id: 'wms'   // レイヤの識別 ID
        });

        // WFS データの読み込み設定
        var vectorSource = new ol.source.Vector({
            loader: function(extent, resolution, projection) {
                // WFS サービスからデータを読み込むための URL を構築
                var url = 'http://localhost:8080/cgi-bin/qgis_mapserv.fcgi.exe?' +
                    'MAP=C:/OSGeo4W/apps/qgis/test2.qgz&' +
                    'SERVICE=WFS&REQUEST=GetFeature&' +
                    'VERSION=1.1.0&TYPENAME=W05-08_13-g_Stream&' +
                    'SRSNAME=EPSG:3857&' +
                    'BBOX=' + extent.join(',') + ',EPSG:3857&' +
                    'outputFormat=text/xml; subtype=gml/3.1.1';
                fetch(url).then(function(response) {
                    return response.text();  // レスポンスをテキストとして取得
                }).then(function(text) {
                    // 取得した WFS データをフィーチャーとして読み込み
                    var features = new ol.format.WFS().readFeatures(text, {
                        dataProjection: 'EPSG:3857',                      // データの投影法
                        featureProjection: map.getView().getProjection()  // マップの投影法
                    });
                    vectorSource.addFeatures(features);  // フィーチャーをソースに追加
                }).catch(function(error) {
                    console.error('Error loading WFS: ', error);  // エラー処理
                });
            },
            strategy: ol.loadingstrategy.bbox  // ローディング戦略（ここでは bbox を使用）
        });

        // WFS レイヤの設定
        var wfsLayer = new ol.layer.Vector({
            source: vectorSource,
            zIndex: 2,  // z-index の設定
            id: 'wfs'  // レイヤの識別 ID
        });

        // マップの設定
        var map = new ol.Map({
            target: 'map',  // マップを表示する要素の ID
            layers: [osmLayer, wmsLayer, wfsLayer],  // マップに追加するレイヤ
            view: new ol.View({
                center: ol.proj.fromLonLat([139.6917, 35.6895]),  // 初期の中心座標（東京）
                zoom: 10  // 初期のズームレベル
            })
        });

        // ID でレイヤを取得する関数
        function getLayerById(id) {
            var layers = map.getLayers().getArray();
            return layers.find(layer => layer.get('id') === id);
        }

        // 初期状態でボタンの状態を更新する関数
        function updateButtonStates() {
            var layers = map.getLayers().getArray();  // マップの全レイヤを取得
            var layerCount = layers.length;  // レイヤの総数

            // 各レイヤに対してボタンの状態を更新
            layers.forEach((layer, index) => {
                var layerId = layer.get('id');  // レイヤの ID を取得
                var downButton = document.getElementById(layerId + 'Down');  // 下ボタンの要素を取得
                var upButton = document.getElementById(layerId + 'Up');  // 上ボタンの要素を取得

                // 最下層のレイヤの場合、下へのボタンを無効化
                if (index === 0) {
                    downButton.disabled = true;
                } else {
                    downButton.disabled = false;
                }

                // 最上層のレイヤの場合、上へのボタンを無効化
                if (index === layerCount - 1) {
                    upButton.disabled = true;
                } else {
                    upButton.disabled = false;
                }
            });
        }

        // レイヤを下に移動する関数
        function moveLayerDown(layer) {
            var layersCollection = map.getLayers();    // マップのレイヤ・コレクションを取得
            var layers = layersCollection.getArray();  // レイヤ・コレクションを配列として取得
            var index = layers.indexOf(layer);         // 移動対象のレイヤのインデックスを取得

            if (index > 0) {  // インデックスが0より大きい場合（最上層でない場合）
                var aboveLayer = layers[index - 1];        // 一つ上のレイヤを取得
                var currentZIndex = layer.getZIndex();     // 現在の z-index を取得
                var aboveZIndex = aboveLayer.getZIndex();  // 上のレイヤの z-index を取得

                // z-index を交換してレイヤの順番を変更
                layer.setZIndex(aboveZIndex);
                layersCollection.remove(layer);
                layersCollection.insertAt(index - 1, layer);
                aboveLayer.setZIndex(currentZIndex);

                // マップの再描画とDOMの更新
                map.render();
                updateDOMOrder();
                updateButtonStates();  // ボタンの状態を更新
            }
        }

        // レイヤを上に移動する関数
        function moveLayerUp(layer) {
            var layersCollection = map.getLayers();
            var layers = layersCollection.getArray();
            var index = layers.indexOf(layer);

            if (index < layers.length - 1) {  // インデックスがレイヤ数未満の場合（最下層でない場合）
                var belowLayer = layers[index + 1];  // 一つ下のレイヤを取得
                var currentZIndex = layer.getZIndex();
                var belowZIndex = belowLayer.getZIndex();

                // z-indexを交換してレイヤの順番を変更
                layer.setZIndex(belowZIndex);
                layersCollection.remove(layer);
                layersCollection.insertAt(index + 1, layer);
                belowLayer.setZIndex(currentZIndex);

                // マップの再描画とDOMの更新
                map.render();
                updateDOMOrder();
                updateButtonStates();  // ボタンの状態を更新
            }
        }

        // DOM の順序を更新する関数
        function updateDOMOrder() {
            var layerElems = Array.from(document.querySelectorAll('#layerControls > div'));
            layerElems.sort((a, b) => {
                var layerA = getLayerById(a.id.replace('Control', ''));
                var layerB = getLayerById(b.id.replace('Control', ''));
                return layerB.getZIndex() - layerA.getZIndex();
            });
            var parent = document.getElementById('layerControls');
            layerElems.forEach(elem => parent.appendChild(elem));
        }

        // 各ボタンにイベントリスナを設定
        document.getElementById('osmDown').addEventListener('click', function() { moveLayerDown(osmLayer); });
        document.getElementById('osmUp').addEventListener('click', function() { moveLayerUp(osmLayer); });
        document.getElementById('wmsDown').addEventListener('click', function() { moveLayerDown(wmsLayer); });
        document.getElementById('wmsUp').addEventListener('click', function() { moveLayerUp(wmsLayer); });
        document.getElementById('wfsDown').addEventListener('click', function() { moveLayerDown(wfsLayer); });
        document.getElementById('wfsUp').addEventListener('click', function() { moveLayerUp(wfsLayer); });

        // レイヤの表示/非表示の切り替え
        document.getElementById('osmVisibility').addEventListener('change', function() {
            osmLayer.setVisible(this.checked);
            toggleOsmOpacityControl(this.checked);
        });
        document.getElementById('wmsVisibility').addEventListener('change', function() {
            wmsLayer.setVisible(this.checked);
            toggleWmsOpacityControl(this.checked);
        });
        document.getElementById('wfsVisibility').addEventListener('change', function() {
            wfsLayer.setVisible(this.checked);
            toggleWfsOpacityControl(this.checked);
        });

        // 透過率の調整
        document.getElementById('osmOpacity').addEventListener('input', function() {
            osmLayer.setOpacity(parseFloat(this.value));
        });
        document.getElementById('wmsOpacity').addEventListener('input', function() {
            wmsLayer.setOpacity(parseFloat(this.value));
        });
        document.getElementById('wfsOpacity').addEventListener('input', function() {
            wfsLayer.setOpacity(parseFloat(this.value));
        });

        // 透過率の表示/非表示の切り替え
        function toggleOsmOpacityControl(visible) {
            var opacityControl = document.querySelector('.slider-label_osm');
            opacityControl.style.display = visible ? 'block' : 'none';  // visible が true なら表示、false なら非表示
        }
        function toggleWmsOpacityControl(visible) {
            var opacityControl = document.querySelector('.slider-label_wms');
            opacityControl.style.display = visible ? 'block' : 'none';
        }
        function toggleWfsOpacityControl(visible) {
            var opacityControl = document.querySelector('.slider-label_wfs');
            opacityControl.style.display = visible ? 'block' : 'none';
        }

        // 初期状態でボタンの状態を更新
        updateButtonStates();
    });
    </script>
</body>
</html>
```
</details>