地図上に固定画像を配置する方法~OpenLayersで図形を描画する⑤

JavaScript

目次

①基本編ではーカーの描画方法についてお伝えいたしました。

マーカーには拡大/縮小しても表示される大きさは変わらないという特徴があります。
しかし、その特徴は以下のような場面で不都合となります。

  • 地図以外の画像を地図の一部として扱いたい
  • 地図の拡大/縮小とともに拡大/縮小される画像を配置したい

そのような場合には、今回ご紹介する固定画像の配置を行う必要があります。

実際にやってみた

画像の拡大・縮小の実験

まずは結果から↓↓↓↓↓

マーカーと画像が配置されています。
こちらを拡大「+」してみます。

固定画像は地図と一緒に拡大されていますが、マーカー画像は拡大されていません。
次は縮小表示「ー」してみます。

マーカーはどの縮尺においても同じ大きさで表示されることがおわかりいただけると思います。

それに対して固定画像の方は地図が縮小されると、地図と一緒に縮小されています

ソースコード

ソースコードはこちらです。

    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.5/ol.css" integrity="sha256-rQq4Fxpq3LlPQ8yP11i6Z2lAo82b6ACDgd35CKyNEBw=" crossorigin="anonymous">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.5/ol.js" integrity="sha256-77IKwU93jwIX7zmgEBfYGHcmeO0Fx2MoWB/ooh9QkBA=" crossorigin="anonymous"></script>
    <!--<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>-->
    <script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
    <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>   -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">

<!-- Latest compiled and minified JavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
  
  
    <div id="map" style="height:300px"></div>
    <div id="popup"></div>
    <script>
    
      // 描画された図形に適用されるスタイル
      var styleFunction = function(feature) {
        var geometry = feature.getGeometry();
        var shape = feature.get('shape');
        var styles = [];
        if (geometry.getType() == 'Point') {
          // 点の場合、マーカー配置
          styles.push(new ol.style.Style({
            image: new ol.style.Icon ({
                anchor: [0.5, 46],
                anchorXUnits: 'fraction',
                anchorYUnits: 'pixels',
                opacity: 0.95,
                src: 'https://openlayers.org/en/latest/examples/data/icon.png'
            })
          }));
        } else if (shape == 'LineString'){
          styles.push(new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#ff0000',
              width: 2
            })
          }));
        } else if (shape == 'Text') {
          var extent = ol.extent.boundingExtent(geometry.getCoordinates()[0]);
          
          var center = [(extent[2] - extent[0]) / 2 + extent[0], (extent[3] - extent[1]) / 2 + extent[1]];
          var centerPixel = map.getPixelFromCoordinate(center);
          var leftTopPixel = map.getPixelFromCoordinate([extent[0], extent[1]]);
          styles.push(new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: 'rgba(0,0,0,1)',
              width: 1
            }),
            text: new ol.style.Text({
              font: 'bold 11px "Open Sans", "Arial Unicode MS", "sans-serif"',
              fill: new ol.style.Fill({
                color: 'black'
              }),
              overflow: true,
              offsetX: leftTopPixel[0] - centerPixel[0],
              offsetY: centerPixel[1] - leftTopPixel[1],
              textAlign: 'left',
              text: feature.get('text')
            })
            
          }));
        } else if (shape == 'Arrow') {
          // 矢印
          styles.push(new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#ffcc33',
              width: 2
            })
          }));
          geometry.forEachSegment(function(start, end) {
            var dx = end[0] - start[0];
            var dy = end[1] - start[1];
            var rotation = Math.atan2(dy, dx);
            // arrows
            styles.push(new ol.style.Style({
              geometry: new ol.geom.Point(end),
              image: new ol.style.Icon({
                src: 'https://openlayers.org/en/latest/examples/data/arrow.png',
                anchor: [0.75, 0.5],
                rotateWithView: true,
                rotation: -rotation
              })
            }));
          });
        } else if (shape == 'Image') {
          // 画像
          return null;
        }else {
          styles.push(new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#ff0000',
              width: 2
            }),
            fill: new ol.style.Fill({
              color: 'rgba(255,0,0,0.3)',
            })
          }));
        }

        return styles;
      };
      
      // 図形を描画するレイヤーを定義する
      var vectorSource = new ol.source.Vector();
      var vectorLayer = new ol.layer.Vector({
        source: vectorSource,
        style: styleFunction
      });
      vectorLayer.set('name', 'vectorLayer');
      
      // 描画用ボタンの配置
      // マーカーボタン
      var MarkerControl = (function (Control) {
        function MarkerControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = '※';
          button.name = 'Point';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '100px';
          element.style.left = '5px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawMarker.bind(this), false);
        }

        if ( Control ) MarkerControl.__proto__ = Control;
        MarkerControl.prototype = Object.create( Control && Control.prototype );
        MarkerControl.prototype.constructor = MarkerControl;

        MarkerControl.prototype.drawMarker = function drawMarker () {
          addInteractions('Point');
        };

        return MarkerControl;
      }(ol.control.Control));
      
      // Linstringボタン
      var LineStringControl = (function (Control) {
        function LineStringControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = '<';
          button.name = 'LineString';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '140px';
          element.style.left = '5px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawLineString.bind(this), false);
        }

        if ( Control ) LineStringControl.__proto__ = Control;
        LineStringControl.prototype = Object.create( Control && Control.prototype );
        LineStringControl.prototype.constructor = LineStringControl;

        LineStringControl.prototype.drawLineString = function drawLineString () {
          addInteractions('LineString');
        };

        return LineStringControl;
      }(ol.control.Control));
      
      // 円ボタン
      var CircleControl = (function (Control) {
        function CircleControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = 'О';
          button.name = 'Circle';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '180px';
          element.style.left = '5px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawCircle.bind(this), false);
        }

        if ( Control ) CircleControl.__proto__ = Control;
        CircleControl.prototype = Object.create( Control && Control.prototype );
        CircleControl.prototype.constructor = CircleControl;

        CircleControl.prototype.drawCircle = function drawCircle () {
          addInteractions('Circle');
        };

        return CircleControl;
      }(ol.control.Control));
      
      // 多角形ボタン
      var PolygonControl = (function (Control) {
        function PolygonControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = '△';
          button.name = 'Polygon';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '220px';
          element.style.left = '5px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawPolygon.bind(this), false);
        }

        if ( Control ) PolygonControl.__proto__ = Control;
        PolygonControl.prototype = Object.create( Control && Control.prototype );
        PolygonControl.prototype.constructor = PolygonControl;

        PolygonControl.prototype.drawPolygon = function drawPolygon () {
          addInteractions('Polygon');
        };

        return PolygonControl;
      }(ol.control.Control));
      
      // テキストボタン
      var TextControl = (function (Control) {
        function TextControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = 'T';
          button.name = 'Text';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '100px';
          element.style.left = '45px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawText.bind(this), false);
        }

        if ( Control ) TextControl.__proto__ = Control;
        TextControl.prototype = Object.create( Control && Control.prototype );
        TextControl.prototype.constructor = TextControl;

        TextControl.prototype.drawText = function drawText () {
          addInteractions('Text');
        };

        return TextControl;
      }(ol.control.Control));
      
      // 矩形ボタン
      var RectangleControl = (function (Control) {
        function RectangleControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = 'ロ';
          button.name = 'Rectangle';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '140px';
          element.style.left = '45px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawRectangle.bind(this), false);
        }

        if ( Control ) RectangleControl.__proto__ = Control;
        RectangleControl.prototype = Object.create( Control && Control.prototype );
        RectangleControl.prototype.constructor = RectangleControl;

        RectangleControl.prototype.drawRectangle = function drawRectangle () {
          addInteractions('Rectangle');
        };

        return RectangleControl;
      }(ol.control.Control));
      
      // 矢印ボタン
      var ArrowControl = (function (Control) {
        function ArrowControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = '→';
          button.name = 'Arrow';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '180px';
          element.style.left = '45px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawArrow.bind(this), false);
        }

        if ( Control ) ArrowControl.__proto__ = Control;
        ArrowControl.prototype = Object.create( Control && Control.prototype );
        ArrowControl.prototype.constructor = ArrowControl;

        ArrowControl.prototype.drawArrow = function drawArrow () {
          addInteractions('Arrow');
        };

        return ArrowControl;
      }(ol.control.Control));

      // 画像ボタン
      var ImageControl = (function (Control) {
        function ImageControl(opt_options) {
          var options = opt_options || {};

          var button = document.createElement('button');
          button.innerHTML = '画';
          button.name = 'Image';
          button.classList.add('map-button');

          var element = document.createElement('div');
          element.className = 'ol-unselectable ol-control';
          element.style.top = '220px';
          element.style.left = '45px';
          element.appendChild(button);

          Control.call(this, {
            element: element,
            target: options.target
          });

          button.addEventListener('click', this.drawImage.bind(this), false);
        }

        if ( Control ) ImageControl.__proto__ = Control;
        ImageControl.prototype = Object.create( Control && Control.prototype );
        ImageControl.prototype.constructor = ImageControl;

        ImageControl.prototype.drawImage = function drawImage () {
          addInteractions('Image');
        };

        return ImageControl;
      }(ol.control.Control));

      // 地図の初期化
      var map = new ol.Map({
        controls: ol.control.defaults().extend([
          // マーカーボタン
          new MarkerControl(),
          // LineStringボタン
          new LineStringControl(),
          // 円ボタン
          new CircleControl(),
          // 多角形ボタン
          new PolygonControl(),
          // テキストボタン
          new TextControl(),
          // 矩形ボタン
          new RectangleControl(),
          // 矢印ボタン
          new ArrowControl(),
          // 画像ボタン
          new ImageControl(),
        ]),
        target: 'map',
        layers: [
          new ol.layer.Tile({
            preload: 4,
            source: new ol.source.OSM()
          }), vectorLayer
        ],
        loadTilesWhileAnimating: true,
        view: new ol.View({
            center: ol.proj.fromLonLat([134.227352, 35.539909]),
            zoom: 13
        })
      });
      
      //var modify = new ol.interaction.Modify({source: vectorSource});
      //map.addInteraction(modify);

      // 図形描画処理
      var draw, snap;
      function addInteractions(type) {
        // 描画モードの切り替え
        if (draw) {
          map.removeInteraction(draw);
          draw = null;
        }
        var shape = type;
        var geometryFunction;
        if (type == 'Text' || type == 'Rectangle' || type == 'Image') {
          type = 'Circle';
          geometryFunction = ol.interaction.Draw.createBox();
        } else if (type == 'Arrow') {
          type = 'LineString';
        }
        
        draw = new ol.interaction.Draw({
          source: vectorSource,
          type: type,
          geometryFunction: geometryFunction,
          geometryName: shape
        });
        
        draw.on('drawend', function(e){
          var shape = e.feature.getGeometryName();
          e.feature.set('shape', shape);

          if (shape == 'Text') {
            // テキストの場合
            inputText(e.feature);
          } else if (shape == 'Image') {
            // 画像の場合
            // 描画した矩形を取得
            var extent = ol.extent.boundingExtent(e.feature.getGeometry().getCoordinates()[0]);

            // 矩形内に画像を配置する
            var imageLayer = new ol.layer.Image({
              source: new ol.source.ImageStatic({
                url: 'https://sun-san-tech.com/wp-content/uploads/2019/12/wave.jpg',
                projection: new ol.proj.Projection({
                  code: 'xkcd-image',
                  units: 'pixels',
                  extent: extent
                }),
                imageExtent: extent
              })
            });
            map.addLayer(imageLayer);
          }
        });
        map.addInteraction(draw);
        
        // マウスカーソルが図形の座標に近づいたときにカーソルを合わせる
        if (!snap) {
          snap = new ol.interaction.Snap({source: vectorSource});
          map.addInteraction(snap);
        }
        
        // 現在の描画モードのボタンを色付けする
        $(".map-button").css('color', 'white');
        $(".map-button[name='" + shape + "']").css('color', 'red');
      }

      var popup = new ol.Overlay({
        element: document.getElementById('popup')
      });
      map.addOverlay(popup);

      function inputText(feature) {
        var element = popup.getElement();
        var coordinate = feature.getGeometry().getCoordinates()[0][3];
        $(element).popover('destroy');
        popup.setPosition(coordinate);
        $(element).popover({
          placement: 'top',
          // animation: false,
          html: true,
          //content:'<div></div>',
          //container: 'body',
          content: '<div id="_popup"></div>'
        }).on('shown.bs.popover', function(e){
          var popover = $(this);
          $(this).parent().find('#_popup').html('<p><textarea id="txt"></textarea></p><p><input type="button" id="ok" value="ok"/>&nbsp<input type="button" id="cancel" value="cancel"/></p>');
          $(this).parent().find('#cancel').on('click', function(e){
            $(element).popover('destroy');
          });
          var txt = $(this).parent('div').find('#txt');
          $(this).parent().find('#ok').on('click', function(e){
            feature.set('text', txt.val());
            $(element).popover('destroy');
          });
        });
        $(element).popover('show').on('shown.bs.popover', function(e){
          var popover = $(this);
          $(this).parent().find('#cancel').on('click', function(e){
            $(element).popover('destroy');
          });
        });
      }
    </script>

操作方法

②テキスト表示で使用した矩形描画機能を使用して、長方形を描画します。

長方形を描画すると、その範囲に画像が配置されます。

動かしてみよう

実際に動作するものがこちらです。
「OpenLayersで図形を描画する」シリーズの集大成になります。

解説

ボタンの配置

368〜400行目でボタンの配置を行なっています。
仕組みはこれまでに扱ってきたボタンと同じのため、説明は省略します。

スタイルの設定

95〜97行目でスタイルの設定を行なっています。

画像は描画領域を指定するのみで、長方形を描画するわけではないので、nullを返却します。

これにより、長方形は領域指定のみに使用され、実際に描画されることはなくなります。

画像の描画

471〜487行目、矩形描画で領域を指定した後の処理で、画像の配置を行います。

473行目、「ol.extent.boundingExtent」という処理を行うことで、描画された長方形の頂点を「extent」(※)という形式で取得できます。

(※)OpenLayersでは長方形を扱う際に、「extent」という概念が出てきます。extentとは[minX, minY,maxX,maxY]各座標を配列で保持しています。
OpenLayersでextentというワードが出てきたら、長方形のことを指すと覚えておいてください。

extentを取得できたら、その情報を取得し、ImageLayerを作成します。
ImageLayerには「url」属性に固定画像のURLを指定することで、その画像をextent内に表示することができます。

487行目の「map.addLayer(imageLayer)」の処理で、地図に上述のImageLayerを配置しています。

以上の処理を行うことで、固定画像の配置を実現しております。

まとめ

今回は矩形描画とImageLayerの組み合わせを用いて、OpenLayersの地図上にオリジナルの画像を配置する方法についてお伝えしました。

OpenLayersの「Draw」機能は描画前、描画後、スタイルの3つの機会で独自の処理を行えるため、図形描画について、とても柔軟なカスタマイズを行うことができます。

今回は5回に分けて以下の描画機能についてお伝えしました。

図形描画についてこれ程の機能を有しているオープンソースのライブラリは他にありません。

ぜひ、これらの機能を活用して地図機能の拡充を試みてください!

OpenLayersについての別記事もぜひご覧ください!

①図形を描画する(マーカー、線、円、多角形)
②テキストを書き込む(矩形)
③フリーハンドで書き込む
④矢印で強調する
⑤固定画像を配置する
⑥図形を選択する
⑦図形を移動させる
⑧固定画像の移動
⑨図形を変形させる
⑩長方形を変形させる
⑪図形を複数選択する
⑫図形をJSON形式で保存&読込
⑬図形を削除する
⑭線のスタイルを設定する
⑮図形の色を設定する

【入門】地図を表示させる方法
ボタンを配置する方法
ミニマップを表示する方法

コメント

タイトルとURLをコピーしました