目次
前回までの記事でOpenLayersにおける図形の描画についてお伝えしました。
今回から描画した図形を選択・移動・変形させる方法についてお伝えします。
マーカーの置き直しや、多角形の角を増やしたりすることができます。
これらを実現することができれば、OpenLayersをお絵描きアプリのように使用することができます。
今回は「選択」についてお伝えします。
動作確認
触れます ↓ ↓ ↓ ↓ ↓
ソースコード
ソースコードはこちらになります。
<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, 20], anchorXUnits: 'fraction', anchorYUnits: 'pixels', opacity: 0.95, src: 'https://sun-san-tech.com/wp-content/uploads/2024/02/marker_blue.png' }) })); } else if (shape == 'LineString'){ styles.push(new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#ff0000', width: 2 }) })); } else if (shape == 'Text') { 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: getTextOffset(feature)[0], offsetY: getTextOffset(feature)[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') { // 画像 styles.push(new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0,0,0,0)', width: 1 }), fill: new ol.style.Fill({ color: 'rgba(0,0,0,0)', }) })); }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; }; function getTextOffset(feature) { var geometry = feature.getGeometry(); 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]]); return [leftTopPixel[0] - centerPixel[0], centerPixel[1] - leftTopPixel[1]]; } var selectStyleFunction = function(feature) { var styles = []; var shape = feature.get('shape'); if (shape == 'Text') { // テキスト選択時 styles.push(new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0,153,255,1)', width: 3 }), fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.3)', }), 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: getTextOffset(feature)[0], offsetY: getTextOffset(feature)[1], textAlign: 'left', text: feature.get('text') }) })); } else if (shape == 'Point') { // マーカー選択時 styles.push(new ol.style.Style({ image: new ol.style.Icon ({ anchor: [0.5, 20], anchorXUnits: 'fraction', anchorYUnits: 'pixels', opacity: 0.95, src: 'https://sun-san-tech.com/wp-content/uploads/2024/02/marker_red.png' }) })); } else { styles.push(new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0,153,255,1)', width: 3 }), fill: new ol.style.Fill({ color: 'rgba(255,255,255,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 select; var selectedFeatures; // 選択ボタン var SelectControl = (function (Control) { function SelectControl(opt_options) { var options = opt_options || {}; var button = document.createElement('button'); button.innerHTML = '選'; button.name = 'Select'; button.classList.add('map-button'); var element = document.createElement('div'); element.className = 'ol-unselectable ol-control'; element.style.top = '60px'; element.style.left = '45px'; element.appendChild(button); Control.call(this, { element: element, target: options.target }); button.addEventListener('click', this.doSelect.bind(this), false); } if ( Control ) SelectControl.__proto__ = Control; SelectControl.prototype = Object.create( Control && Control.prototype ); SelectControl.prototype.constructor = SelectControl; SelectControl.prototype.doSelect = function doSelect () { // 選択モードと描画モードの切り替え if (!select) { changeMode(); } }; return SelectControl; }(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(), // 選択ボタン new SelectControl(), ]), 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; } if (select) { changeMode(); } 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"/> <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'); }); }); } function changeMode() { if (select) { // 選択モードから描画モードに切り替え map.removeInteraction(select); select = null; } else { // 描画モードから選択モードへの切り替え if (draw) { map.removeInteraction(draw); draw = null; } select = new ol.interaction.Select({ layers: [vectorLayer], style: selectStyleFunction, }); selectedFeatures = select.getFeatures(); map.addInteraction(select); $(".map-button").css('color', 'white'); $(".map-button[name='Select']").css('color', 'red'); } } </script>
動作方法
マーカーの選択
マーカーを2つ配置します。
「選」ボタンを押下し、選択モードに移行し、マーカーをクリックします。
青いマーカーが赤いマーカー画像に切り替わりました。
これがマーカーの選択です。
マーカー以外の選択
多角形を2つ描画します。
「選」ボタンを押下し、選択モードに移行し、片方の図形をクリックします。
選択時のスタイルが適用されました。
こちらがマーカー以外の選択状態になります。
解説
選択モードのスタイルを設定する
125~173行目の処理で図形が選択された際のスタイルを設定しています。
線は水色、塗りつぶしは薄い白色、マーカー画像は赤色の別画像を使用するように指定しています。
選択ボタンの追加
464~502行目でボタンの定義を行っております。
495行目からの処理で、ボタンが押下された際に選択モードに移行する、changeMode()をコールしております。
523行目の処理でボタンを地図上に追加しております。
選択モードへ移行
647~667行目で選択モードと描画モードの切り替えを行っております。
前回までの記事でお伝えした「図形描画モード」と今回の「選択モード」の切り替えです。
選択モードの移行に必要な処理は以下になります。
- DrawInteractionの除去
- SelectInteractionの追加
これらの処理を行うことにより、図形描画モードから選択モードに移行しております。
まとめ
今回はOpenLayersにおいて、図形を選択する動作についてお伝えいたしました。
選択というと、Excelを使用している方からすれば「できて当然」の動作です。
しかし、OpenLayersにおいては上記でお伝えしたように、1つ1つソースコードで指定しない限り実現できません。
OpenLayersプログラミングの難しさを象徴しているとも言えます。
今回お伝えした「図形を選択する」という動作は、図形の移動や変形につながる重要なステップですので、マスターしておきましょう!
OpenLayersについての別記事もぜひご覧ください!
①図形を描画する(マーカー、線、円、多角形)
②テキストを書き込む(矩形)
③フリーハンドで書き込む
④矢印で強調する
⑤固定画像を配置する
⑥図形を選択する
⑦図形を移動させる
⑧固定画像の移動
⑨図形を変形させる
⑩長方形を変形させる
⑪図形を複数選択する
⑫図形をJSON形式で保存&読込
⑬図形を削除する
⑭線のスタイルを設定する
⑮図形の色を設定する
【入門】地図を表示させる方法
ボタンを配置する方法
ミニマップを表示する方法
コメント