googlemap のポリゴンを編集してダウンロードし直す

2022年5月3日

行政区画境界の緯度経度データを Linked Open Addresses Japan から取得してきました。ちょっと手直ししないと使えなそうなデータがあったので、googlemap 上でポリゴンを編集してダウンロードしなおせるようにしたのですがその時のコードを残しておこうと思います。

JSON アップロード

編集したい JSON をアップロードします。
アップロードすると地図上にポリゴンが表示されます。

JSON は下記のような階層です。

[
    {
        "lat": 24.284078400000002,
        "lng": 153.97357438
    },
    {
        "lat": 24.28297405,
        "lng": 153.97858222
    },
    {
        "lat": 24.282955490000003,
        "lng": 153.97858711
    },
    ...
]

ポリゴン編集

ポリゴンの頂点をドラッグするとポリゴンの形を変えられます。
ポリゴンの頂点をダブルクリックすると頂点を削除できます。

ドラッグはもともとの機能ですがダブルクリックで削除できるようにするにはイベントを作成しないといけません。

ポリゴン座標をまとめて削除

Linked Open Addresses Japan の東京都のデータを見てみると「伊豆諸島」「小笠原諸島」など不要な座標データもありました。

前述のダブルクリックでは削除が大変なので、ポリゴンの座標をまとめて削除できる機能を追加しました。

JSON ダウンロード

編集した JSON をダウンロードできます。

コード

エラー処理とか入れてなかったりしますが、ざっくりと。。。

<template>
    <div>
        <div class="map-container">
            <div class="map-wrapper">
                <div id="map" class="map"></div>
            </div>
        </div>
        <div id="utils-container" class="utils-container">
            <div class="utils-wrapper">
                <input style="display: none" type="file" ref="file" id="upload-json" class="" name="upload-json" v-on:change="uploadJson">
                <label for="upload-json"><div class="button">JSON アップロード</div></label>
                <div><button @click="downloadJson()" :disabled="!polygon">ポリゴン座標をダウンロード</button></div>
                <div><button @click="drawRectangle()" :disabled="!polygon || drawingManager">指定範囲のポリゴンの頂点を削除</button></div>
                <div><button @click="cancelDrawRectangle()" :disabled="!drawingManager">指定範囲の頂点削除モード解除</button></div>
                <div><button @click="restorePolygon()" :disabled="!isDeletedVertexes">{{ isRestoreMessage() }}</button></div>
            </div>
        </div>
        <!-- モーダル -->
        <div class="busy-background" v-if="isBusy">
            <div class="busy-window">
                <div class="busy-content">
                    <div class="busy-title">Now Loading ...</div>
                    <div class="gif-wrapper"><img class="icon" src="@/assets/earth.gif"></div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                map: null,
                drawingManager: null,
                currentLat: 35.6807944, // 値が大きい程北 小さい程南
                currentLng: 139.7670774, // 値が小さい程西 大きいほど東
                polygon: null,
                fileName: null,
                currentCoordinates: [],
                oldCoordinates: [],
                isDeletedVertexes: false,
                isRestore: true,
                isBusy: false,
            }
        },
        mounted (){
            this.initMap();
        },
        watch: {
            currentCoordinates: {
                handler(newVal, oldVal) {
                    this.oldCoordinates = oldVal;
                }
            },
            deep: true,
        },
        methods: {
            initMap: function () {
                /**
                 * 地図を描画する。
                 */
                let timer = setInterval(() => {
                    if(window.google){
                        clearInterval(timer);
                        this.map = new window.google.maps.Map(document.getElementById('map'), {
                            gestureHandling: 'greedy',
                            mapTypeControl: false,
                            zoomControl: false,
                            streetViewControl: false,
                            center: {lat: this.currentLat, lng: this.currentLng},
                            zoom: 12,
                            clickableIcons: false,
                            fullscreenControl: false,
                        });
                    }
                }, 500)
            },
            uploadJson: function(e) {
                /**
                 * JSON ファイルからポリゴンを描画する。
                 */
                // Loding を表示する。
                this.isBusy=true;
                // 既にポリゴンがある場合は削除する。
                if(this.polygon) {
                    this.polygon.setMap(null);
                    this.polygon = null;
                }
                // ファイルを読み込んだらポリゴンを描画する。
                var fileReader = new FileReader();
                fileReader.addEventListener('load', (e) => {
                    this.drawPolygon(JSON.parse(e.target.result), true).then(() => {
                        this.isBusy = false;
                    });
                });
                this.fileName = e.target.files[0].name;
                fileReader.readAsText(e.target.files[0]);
            },
            downloadJson: function() {
                /**
                 * JSON ファイルをダウンロードする。
                 */
                // ポリゴンから座標 JSON を作成する。
                let polygonCoordinates = []
                let polygonVertexes = this.polygon.getPath().getArray()
                for(let i in polygonVertexes) {
                    let polygonCoordinate = {
                        lat: polygonVertexes[i].lat(),
                        lng: polygonVertexes[i].lng()
                    }
                    polygonCoordinates.push(polygonCoordinate);
                }
                let downloadCoordinates = JSON.stringify(polygonCoordinates);
                // ファイルをダウンロードする。
                var downLoadLink = document.createElement('a');
                downLoadLink.download = this.fileName;
                downLoadLink.href = URL.createObjectURL(new Blob([downloadCoordinates], {type: 'text.plain'}));
                downLoadLink.dataset.downloadurl = ['text/plain', downLoadLink.download, downLoadLink.href].join(':');
                downLoadLink.click();
            },
            drawPolygon(polygonCoordinates, isUpload=false) {
                /**
                 * ポリゴンを描画する。
                 */
                return new Promise((resolve, reject) => {
                    this.polygon = new window.google.maps.Polygon({
                        paths: polygonCoordinates, // ポリゴンの配列
                        strokeColor: '#FF0000', // ライン色(#RRGGBB形式)
                        strokeOpacity: 0.8, // ライン透明度 0.0~1.0(デフォルト)
                        strokeWeight: 2, // ライン太さ(単位ピクセル)
                        fillColor: '#FF0000', // ポリゴン領域色(#RRGGBB形式)
                        fillOpacity: 0.35, // ポリゴン領域透明度 0.0~1.0(デフォルト)
                        editable: true, // ポリゴン編集の可否
                    });
                    this.polygon.setMap(this.map);
                    // JSON 読み込み時のみポリゴン全体が映るように地図の範囲を拡張する。
                    if (isUpload) { 
                        // 地図の範囲を拡張する bounds を用意する。
                        var bounds = new google.maps.LatLngBounds();
                        // bounds に拡張する座標を追加する。
                        let polygonVertexes = this.polygon.getPath().getArray()
                        for(let i in polygonVertexes) {
                            var latLng = new google.maps.LatLng(polygonVertexes[i].lat(), polygonVertexes[i].lng()) ;
                            bounds.extend(latLng);
                        }
                        // 地図を拡張する。
                        this.map.fitBounds(bounds);
                    }
                    // 少し時間をおかないと読込み中が出ない。
                    setTimeout(()=>{
                        resolve()
                    }, 500);
                    // 現在描画されたポリゴンの情報を保持する。
                    this.currentCoordinates = polygonCoordinates;
                    // ポリゴンの頂点をダブルクリックで削除する。
                    google.maps.event.addListener(this.polygon, 'dblclick', (event) => { 
                        if (event.vertex == undefined) { 
                            return; 
                        } else { 
                            this.polygon.getPath().removeAt(event.vertex)
                        } 
                    });
                });
            },
            drawRectangle: function() {
                /**
                 * レクタングルを描画する。
                 * レクタングル内に存在するポリゴンの頂点を破棄してポリゴンを再描画する。
                 */
                this.drawingManager = new google.maps.drawing.DrawingManager({
                    drawingMode: google.maps.drawing.OverlayType.RECTANGLE, // 描画のタイプ(最初からポリゴン描画のタイプを指定するなら次の通り「google.maps.drawing.OverlayType.POLYGON」)
                    drawingControl: false, // 描画ツールボックス表示の可否
                    map: this.map, // 描画する地図
                    rectangleOptions: {   
                        strokeColor: '#0000FF', // ライン色(#RRGGBB形式)
                        strokeOpacity: 0.8, // ライン透明度 0.0~1.0(デフォルト)
                        strokeWeight: 2, // ライン太さ(単位ピクセル)
                        fillColor: '#0000FF', // ポリゴン領域色(#RRGGBB形式)
                        fillOpacity: 0.35, // ポリゴン領域透明度 0.0~1.0(デフォルト)
                        draggable: false, // ポリゴン描画後の移動の可否
                        editable: false, // ポリゴン描画後の変形の可否
                    },
                });
                // レクタングル描画終了時のイベントを設定
                google.maps.event.addListener(this.drawingManager, 'rectanglecomplete', this.onRectangleComplete)
            },
            cancelDrawRectangle: function() {
                /**
                 * レクタングル描画モードを終了する。
                 */
                // 元に戻すが使用できるのはレクタングル描画モードの時のみ。
                this.isDeletedVertexes = false;
                this.drawingManager.setDrawingMode(null);
                this.drawingManager = null;
            },
            onRectangleComplete: function(rectangle) {
                /**
                 * レクタングルの描画が完了したら、4 地点の座標を取得する。
                 * レクタングル内に存在するポリゴンの頂点を破棄してポリゴンを再描画する。
                 */
                this.rectangle = rectangle;
                let bounds = rectangle.getBounds();
                let NE = bounds.getNorthEast();
                let SW = bounds.getSouthWest();
                let rectangleVertexes = {};
                rectangleVertexes['north'] = NE.lat();
                rectangleVertexes['east'] = NE.lng();
                rectangleVertexes['sourth'] = SW.lat();
                rectangleVertexes['west'] = SW.lng();
                this.deletePolygonVertexes(rectangleVertexes);
            },
            deletePolygonVertexes: function(rectangleVertexes) {
                /**
                 * レクタングル内に存在するポリゴンの頂点を破棄してポリゴンを再描画する。
                 */
                let polygonCoordinates = []
                let polygonVertexes = this.polygon.getPath().getArray()
                for(let i in polygonVertexes) {
                    if(
                        polygonVertexes[i].lat() < rectangleVertexes['north'] &&
                        polygonVertexes[i].lat() > rectangleVertexes['sourth'] &&
                        polygonVertexes[i].lng() < rectangleVertexes['east'] &&
                        polygonVertexes[i].lng() > rectangleVertexes['west'] 
                    ) {
                        // レクタングル内に座標があった場合は、元に戻せるフラグをオンにする。
                        this.isDeletedVertexes = true;
                    } else {
                        let polygonCoordinate = {
                            lat: polygonVertexes[i].lat(),
                            lng: polygonVertexes[i].lng()
                        }
                         polygonCoordinates.push(polygonCoordinate);
                    }
                }
                this.rectangle.setMap(null);
                this.polygon.setMap(null);
                this.rectangle.setMap(null);
                this.drawPolygon(polygonCoordinates)
            },
            restorePolygon: function() {
                /**
                 * 指定範囲のポリゴンの頂点を削除出来るモードの時はポリゴンを「元に戻す」または「元に戻したのをキャンセルする」が出来る。
                 */
                if(this.polygon) {
                    this.polygon.setMap(null);
                    this.polygon = null;
                }
                this.isRestore = !this.isRestore;
                this.drawPolygon(this.oldCoordinates)
            },
            isRestoreMessage: function() {
                if(this.isRestore) {
                    return 'ポリゴンを元に戻す';
                } else {
                    return '元に戻したのをキャンセル';
                }
            }
        }
    }
YouTube

2022年5月3日