🗾

React + maplibre-gl-jsで複数レイヤの表示・非表示を制御する

2024/07/26に公開

前回、PMTilesに複数のレイヤ

  • 行政区画
  • ハザードマップ

をまとめて、maplibre-gl-jsから表示するところまでやりました

今回は、表示するレイヤを制御する実装をしていきます

実装サンプルを調べるとプレーンなjsでの実装が多いのですが今回はReactを使います

それと、複数レイヤを制御するにあたって実装を少しいじりました
この実装であれば新しいレイヤを後で追加したいとなったときにもスムーズにできると思います

実装サンプル

import React, { useRef, useEffect, useState } from 'react';
import maplibregl, { RasterSourceSpecification, VectorSourceSpecification, SourceSpecification } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { Protocol, PMTiles } from 'pmtiles';

const PMTILES_URL = "PMTilesのパス";

const LABELS: { [key: string]: string } = {
  "gyousei-kukaku": "行政区画",
  "hazard-map": "浸水エリア"
};

const sourceSetting: { [key: string]: SourceSpecification } = {
  "osm": {
    "type": "raster",
    "tiles": ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
    "tileSize": 256,
    "attribution": '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'
  } as RasterSourceSpecification,
  "gyousei-kukaku": {
    "type": "vector",
    "url": "pmtiles://" + PMTILES_URL,
    "attribution": '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
  } as VectorSourceSpecification,
  "fukuoka-hazardmap": {
    "type": "vector",
    "url": "pmtiles://" + PMTILES_URL,
    "attribution": '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
  } as VectorSourceSpecification
};

// 背景地図の設定
const baseLayer: maplibregl.LayerSpecification[] = [
  {
    "id": "osm-tiles",
    "type": "raster",
    "source": "osm",
    "minzoom": 0,
    "maxzoom": 19,
  }
];

// PMTilesから取得して表示、非表示を制御するレイヤ達
const otherLayers: maplibregl.LayerSpecification[] = [
  {
    "id": "gyousei-kukaku",
    "source": "gyousei-kukaku",
    "source-layer": "N0323_230101",
    "type": "fill",
    "paint": {
      "fill-color": "#00ffff",
      "fill-opacity": 0.4,
      "fill-outline-color": "#ff0000",
    },
    "layout": {
      "visibility": "none" // 最初から表示する場合は "visible"に
    }
  },
  {
    "id": "hazard-map",
    "source": "fukuoka-hazardmap",
    "source-layer": "fukuokahazardmap",
    "type": "fill",
    "paint": {
      "fill-color": "#ff0000",
      "fill-opacity": 0.4,
      "fill-outline-color": "#0000ff",
    },
    "layout": {
      "visibility": "none" // 最初から表示する場合は "visible"に
    }
  }
];

type VisibleType = {
  key: string;
  value: boolean;
};

const MapComponent: React.FC<{}> = () => {
  const mapContainer = useRef(null);
  const map = useRef<any>(null);
  const [mapState, setMapState] = useState({ lat: 33.5676, lon: 130.4102, zoom: 9 });
  const layersSetting = [...baseLayer, ...otherLayers];
  const initVisible = otherLayers.map(layer => ({
    key: layer.id,
    value: layer.layout?.visibility === 'visible'
  }));
  
  const [visibleObj, setVisibleObj] = useState<VisibleType[]>(initVisible);
  
  useEffect(() => {
    if (map.current) return; // initialize map only once

    const protocol = new Protocol();
    maplibregl.addProtocol("pmtiles", protocol.tile);
    const p = new PMTiles(PMTILES_URL);
    protocol.add(p);

    map.current = new maplibregl.Map({
      container: mapContainer.current!,
      center: [mapState.lon, mapState.lat],
      zoom: mapState.zoom,
      style: {
        version: 8,
        sources: sourceSetting,
        layers: layersSetting
      }
    });

    map.current.on('move', () => {
      setMapState({
        lat: Number(map.current.getCenter().lat.toFixed(4)),
        lon: Number(map.current.getCenter().lng.toFixed(4)),
        zoom: Number(map.current.getZoom().toFixed(2))
      });
    });

    map.current.on('load', () => {
      // レイヤーの初期表示状態を設定
      visibleObj.forEach(function (obj) {
        map.current.setLayoutProperty(obj.key, 'visibility', obj.value ? 'visible' : 'none');
      });
    });
  }, []);

  useEffect(() => {
    if (map.current && map.current.isStyleLoaded()) {
      visibleObj.forEach(function (obj) {
        map.current.setLayoutProperty(obj.key, 'visibility', obj.value ? 'visible' : 'none');
      });
    }
  }, [visibleObj]);

  const handleVisibilityChange = (key: string) => {
    setVisibleObj(prevState =>
      prevState.map(item =>
        item.key === key ? { ...item, value: !item.value } : item
      )
    );
  };

  return (
    <div className="App">
      <div className="control-panel">
      {visibleObj.map((obj) => (
          <label key={obj.key}>
            <input
              type="checkbox"
              checked={obj.value}
              onChange={() => handleVisibilityChange(obj.key)}
            />
            {LABELS[obj.key] || obj.key}
          </label>
        ))}
      </div>
      <div ref={mapContainer} className="map-container" style={{ height: '70vh', width: '100%' }} />
    </div>
  );
};

export default MapComponent;

index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.map-container {
  height: 600px;
  }


.sidebar {
  background-color: rgba(35, 55, 75, 0.9);
  color: #fff;
  padding: 6px 12px;
  font-family: monospace;
  z-index: 1;
  position: absolute;
  top: 0;
  left: 0;
  margin: 12px;
  border-radius: 4px;
}

.control-panel {
  position: absolute;
  top: 20px;
  left: 20px;
  padding: 20px;
  margin-bottom: 20px;
  width: 20%;
  background-color: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  border-radius: 3px;
  word-break: break-all;
  z-index: 1000; 
  text-align: left; 
  display: flex;
  flex-direction: column; 
}

動作例

Fusic 技術ブログ

Discussion