GIS奮闘記

元GISエンジニアの技術紹介ブログ。主にPythonを使用。

スポンサーリンク

piexif を使って画像に付与されているジオタグからポイントを作成して Shape ファイルとして出力する方法

さて、本日は前回のエントリーに続きジオタグのお話です。前回は画像にジオタグを追加する方法を紹介しましたが(以下エントリー参照)、今回は画像に付与されているジオタグを抽出する方法を紹介しようと思います。また、その緯度経度を使用してポイントを作成しShape ファイルとして出力してみようと思います。

www.gis-py.com

使用するライブラリ

piexif
GDAL

piexif で画像に付与されているジオタグを抽出して、その情報を GDAL を使ってポイントを作成し、Shapeファイルとして抽出してみようと思います。なお、ポイント作成とShape 出力の部分は以下エントリーをベースに書いてみようと思います。

www.gis-py.com

使用する画像

こんなサイトがあったので、使ってみました。

www.geoimgr.com

世界各地の風景写真を掲載しているサイトですね。これは京都の桂川の写真らしいです。

f:id:sanvarie:20191026144148p:plain

緯度経度も設定されていることがわかります。

f:id:sanvarie:20191027085913p:plain

こんな感じにデータを格納してみました。これらのジオタグを抽出して、ポイントとして Shape ファイルを作成してみようと思います。

f:id:sanvarie:20191026142150p:plain

実行環境

Windows 10
Python 3.6.6
piexif 1.1.3
GDAL 2.1.1
ArcGIS Pro 2.4

サンプルコード

import os
from glob import glob
import piexif
import osgeo.ogr as ogr

folder = r"D:\data\picture"
shape_file = r"D:\data\shape\geotag.shp"

def make_shape_for_point(info):
    """ポイントShapeファイルを作成"""

    # shapefileドライバ
    driver = ogr.GetDriverByName("ESRI Shapefile")

    # Shape出力先を設定
    data_source = driver.CreateDataSource(shape_file)

    # レイヤ作成
    layer = data_source.CreateLayer("geotag",  geom_type= ogr.wkbPoint)

    # フィールド追加
    field_name = ogr.FieldDefn("file_name", ogr.OFTString)
    field_name.SetWidth(24)
    layer.CreateField(field_name)

    layer.CreateField(ogr.FieldDefn("longitude", ogr.OFTReal))
    layer.CreateField(ogr.FieldDefn("latitude", ogr.OFTReal))

    # Shape作成
    for l in info:
      # フィーチャ作成
      feature = ogr.Feature(layer.GetLayerDefn())

      # 属性を設定
      feature.SetField("file_name", l[2])
      feature.SetField("longitude", l[1])
      feature.SetField("latitude", l[0])

      # 作成するポイントに緯度経度をセット
      wkt = "POINT(%f %f)" %  (float(l[1]) , float(l[0]))

      # ポイント作成
      point = ogr.CreateGeometryFromWkt(wkt)
      feature.SetGeometry(point)
      layer.CreateFeature(feature)
      feature = None

    data_source = None

def conv_deg(v):
    """緯度、経度を60進法(度分秒)→10進法に変換"""

    d = float(v[0][0]) / float(v[0][1])
    m = float(v[1][0]) / float(v[1][1])
    s = float(v[2][0]) / float(v[2][1])
    return d + (m / 60.0) + (s / 3600.0)

def read_geotag():
    """画像を読み込んで緯度経度を取得"""

    info = []

    # 指定するフォルダ内のjpgを取得
    for jpg in glob(folder + "\*.jpg"):

        # 画像を読み込む
        exif_dict = piexif.load(jpg)

        if (len(exif_dict["GPS"]) > 0):

            # 緯度、経度を60進法(度分秒)→10進法に変換
            lat = conv_deg(exif_dict["GPS"][2])
            lon = conv_deg(exif_dict["GPS"][4])

            # ファイル名を取得
            file_name = os.path.basename(jpg)

            info.append([lat, lon, file_name])

    return info

make_shape_for_point(read_geotag())

結果を確認するとこのように Shape ファイルが出力されたことがわかります。

f:id:sanvarie:20191026143502p:plain

出力された Shape ファイルを ArcGIS Pro で読み込んでみると、それっぽい場所にポイントが作成されていることがわかります。

f:id:sanvarie:20191026143133p:plain

京都の桂川のデータを確認してみると、場所は問題なさそうです。また、属性もしっかり格納されていますね。

f:id:sanvarie:20191026144259p:plain

このように piexif を使用すれば前回のようにジオタグを画像に追加したり、今回のように画像に付与されているジオタグを抽出することが簡単にできます。また、 GDAL を使用すれば、その抽出した情報からポイントを作成し、Shape ファイルとして出力することも簡単にできます。

二回連続でジオタグに関して書いてみましたが、いかがでしたでしょうか?もし他に何か知りたいことなどがありましたらコメントしていただければと思います。本日は以上です。

piexif を使って画像にジオタグを追加する方法

さて、本日は 久しぶりに Python を使ってみようと思います。piexif というライブラリがあるのですが、これを使って画像にジオタグを追加する方法を紹介します。ライブラリの詳細は以下のリンクを参照してください。

piexif.readthedocs.io

今回のエントリーに至った経緯

実は以下のエントリーで過去にも同じことをしていたのですが、最近そのコードが動かないことが判明しました(当時使用していたライブラリ(pyexiv2)の仕様が変わった?!もしくは何か依存ライブラリが足りてないのか調べても原因はわかりませんでした・・・)。今回はせっかくなので、別のライブラリ(piexif) を使ってみようと思います。

www.gis-py.com

www.gis-py.com

ジオタグとは

ジオタグとは、写真や動画、あるいは SNS の投稿といったさまざまなメディアに追加することができる、位置情報 (緯度・経度) を示すメタデータのことです。

使用するライブラリ

piexif
Pillow

実行環境

Windows 10
ArcGIS Pro 2.4
Python 3.6.6
piexif 1.1.3
Pillow 6.1.0

使用するデータ

以下の画像を使用します。

f:id:sanvarie:20191026105621p:plain

使用する緯度経度

緯度: 35.678601
経度: 139.740286

ここがどこかといいますと永田町駅ですね。

f:id:sanvarie:20191026091618p:plain

サンプルコード

1ファイルだけを対象としたサンプルコードです。

from fractions import Fraction
import piexif
from PIL import Image

in_file = r"D:\data\picture\arcgis.jpg"         # 使用する画像
out_file = r"D:\data\picture\arcgis_geotag.jpg" # 出力する画像
lat = 35.678601
lon = 139.740286

def to_deg(value, loc):
    """緯度、経度を10進法→60進法(度分秒)に変換"""
    if value < 0:
        loc_value = loc[0]
    elif value > 0:
        loc_value = loc[1]
    else:
        loc_value = ""
    abs_value = abs(value)
    deg =  int(abs_value)
    t1 = (abs_value-deg)*60
    min = int(t1)
    sec = round((t1 - min)* 60, 5)
    return (deg, min, sec, loc_value)

def change_to_rational(number):
    """有理数に変換"""
    f = Fraction(str(number))
    return (f.numerator, f.denominator)

def atattch_geotag(file_name, lat, lng):
    """ジオタグに追加する情報を作成"""
    lat_deg = to_deg(lat, ["S", "N"])
    lng_deg = to_deg(lng, ["W", "E"])

    # 緯度、経度を10進法→60進法(度分秒)に変換
    exiv_lat = (change_to_rational(lat_deg[0]), change_to_rational(lat_deg[1]), change_to_rational(lat_deg[2]))
    exiv_lng = (change_to_rational(lng_deg[0]), change_to_rational(lng_deg[1]), change_to_rational(lng_deg[2]))

    gps_ifd = {
        piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
        piexif.GPSIFD.GPSLatitudeRef: lat_deg[3],
        piexif.GPSIFD.GPSLatitude: exiv_lat,
        piexif.GPSIFD.GPSLongitudeRef: lng_deg[3],
        piexif.GPSIFD.GPSLongitude: exiv_lng,
    }

    exif_dict = {"GPS": gps_ifd}
    exif_bytes = piexif.dump(exif_dict)

    # ジオタグ付きの画像を作成
    create_picture_geotag(exif_bytes)

def create_picture_geotag(exif_bytes):
    """ジオタグを貼り付けた画像を作成"""
    im = Image.open(in_file)
    im.save(out_file, exif = exif_bytes)

atattch_geotag(in_file, lat, lon)

結果を確認してみると、出力された画像にジオタグが追加されていることがわかります。

f:id:sanvarie:20191026103815p:plain

追加されたジオタグが正しいか確認

ArcGIS Pro を使って追加されたジオタグが正しいか確認してみます。「ジオタグ付き写真 → ポイント」というジオプロがあるので、ArcPy を使って実行してみようと思います。

import arcpy
arcpy.GeoTaggedPhotosToPoints_management(r"D:\data\picture", r"D:\Sample\Sample.gdb\python_GeoTaggedPhotosToPoints", "", "ONLY_GEOTAGGED", "ADD_ATTACHMENTS")

設定した緯度経度(永田町)にポイントが作成されました。

f:id:sanvarie:20191026104804p:plain

ポップアップを構成して属性ウインドウに写真を表示してみました。
f:id:sanvarie:20191026105055p:plain

この piexif というライブラリですが、とても簡単に画像にジオタグを追加できる優秀なライブラリということがわかりました。以下のエントリーで画像に付与されているジオタグを抽出する方法を紹介しているので、興味のある方はぜひ読んでみてください。

www.gis-py.com

本日は以上です。

ArcGIS API for JavaScript でグラフィックを使ってジオメトリを作図してみよう

さて、本日も ArcGIS API for JavaScript について書いてみようと思います。今回はグラフィックを使ってジオメトリを作図してみようと思います。ただ、単純にグラフィックを使ってジオメトリを作図するだけなので、属性を付与したり、作図したジオメトリを保存したりはしません。一時作図のようなイメージでしょうか。

今回使用するソース

前回のエントリーをベースに今回使用する処理を書き足そうと思います。

www.gis-py.com

完成イメージ

作図ボタンを追加しました。

f:id:sanvarie:20191020104832p:plain

作図ボタンを押すとこのようにセレクトボックスが表示されます。作図するジオメトリの種類を選んで実行ボタンを押すと作図ができるようになります。

f:id:sanvarie:20191020104959p:plain

実行環境

ArcGIS API for JavaScript 4.13
Chrome

ポイントとなる処理

簡単にではありますが、以下の実装について解説していきます。

1.Draw クラスとGraphic クラスを使用
2.実行ボタン押下時の処理
3.ポイントの作図
4.ラインの作図
5.ポリゴンの作図
6.テキストの作図

Draw クラスとGraphic クラスを使用

今回の要件を満たすには Draw クラスとGraphic クラスを使用する必要があります。

1.Draw クラス
グラフィックを作図するためのクラスです。

var draw = new Draw({
  view: view
});

2.Graphic クラス
グラフィックオブジェクトを作成するためのクラスです。

var graphic = new Graphic({
  geometry: point,
  symbol: {
    type: "simple-marker",
    style: "square",
    color: "red",
    size: "16px",
    outline: { 
      color: [255, 255, 0],
      width: 3
    }
  }
});

実行ボタン押下時の処理

セレクトボックスの選択肢によって以下のように分岐するようにしました。

var sketchToggle= document.getElementById("executeSketch"); // 実行ボタン
on(sketchToggle, "click", function() {
  var geometryType = document.getElementById("geometryType").value;
    
  if(geometryType == "ポイント"){
    enableCreatePoint();
  } else if (geometryType == "ライン"){
    enableCreateLine();
  } else if (geometryType == "ポリゴン"){
    enableCreatePolygon();
  } else if (geometryType == "テキスト"){
    enableCreateText();
  }
});

ポイントの作図

draw.create("point") でポイントを作図することができます。そして、最後に view.graphics.add(graphic); で作図したポイントをマップビューに追加します。

function enableCreatePoint() {
  var action = draw.create("point");
  action.on("draw-complete", function (evt) {
    createPointGraphic(evt.coordinates);
  });
}

function createPointGraphic(coordinates){
  var point = {
    type: "point", 
    x: coordinates[0],
    y: coordinates[1],
    spatialReference: view.spatialReference
  };

  var graphic = new Graphic({
    geometry: point,
    symbol: {
      type: "simple-marker",
      style: "square",
      color: "red",
      size: "16px",
      outline: { 
        color: [255, 255, 0],
        width: 3
      }
    }
  });
  view.graphics.add(graphic);
}

ラインの作図

ポイントの作図と基本は同じです。ただ、ラインなのでクリックするごとにラインのセグメントを作成する必要があります。今回は実装上の都合上、ラインはマップ上に一つしか作図できません(二個目を作図しようとすると一個目は削除される)。

function enableCreateLine() {
  var action = draw.create("polyline");
    view.focus();
    action.on(
      [
        "vertex-add", // Fキー押下で頂点追加
        "vertex-remove", // Zキー押下で頂点削除
        "cursor-update",
        "draw-complete"
      ],
      createPolylineGraphic
   );
}

function createPolylineGraphic(event) {
  var vertices = event.vertices;
  view.graphics.removeAll();

  var graphic = new Graphic({
    geometry: {
      type: "polyline",
      paths: vertices,
      spatialReference: view.spatialReference
    },
    symbol: {
      type: "simple-line",
      color: "red",
      width: 4,
      cap: "round",
      join: "round"
    }
  });

  view.graphics.add(graphic);
}

ポリゴンの作図

ラインの作図とほぼ同じです。なので、ここは共通化した方がいいかもしれないですね。こちらも実装上の都合でマップ上に一つのポリゴンのみが作図可能です。

function enableCreatePolygon() {
  var action = draw.create("polygon");
    view.focus();
    action.on(
      [
        "vertex-add", // Fキー押下で頂点追加
        "vertex-remove", // Zキー押下で頂点削除
        "cursor-update",
        "draw-complete"
      ],
      createPolygonGraphic
   );
}

function createPolygonGraphic(event) {
  var vertices = event.vertices;
  view.graphics.removeAll();

  var graphic = new Graphic({
    geometry: {
      type: "polygon",
      rings: vertices,
      spatialReference: view.spatialReference
    },
    symbol: {
      type: "simple-fill", 
      color: "red",
      style: "solid",
      outline: {  
        color: [255, 255, 0],
        width: 3
      }
    }
  });

  view.graphics.add(graphic);
}

テキストの作図

基本はポイントの作図と同じです。今回はテキストの中身は「GIS奮闘記」固定にしましたが、テキストボックスを作ってそこの値をとってくるようにすれば完璧かと思います。

function enableCreateText() {
  var action = draw.create("point");
  action.on("draw-complete", function (evt) {
    createTextGraphic(evt.coordinates);
  });
}

function createTextGraphic(coordinates){
  var point = {
    type: "point", 
    x: coordinates[0],
    y: coordinates[1],
    spatialReference: view.spatialReference
  };

  var textSymbol = {
    type: "text", 
    color: "red",
    haloColor: "black",
    haloSize: "1px",
    text: "GIS奮闘記",
    xoffset: 3,
    yoffset: 3,
    font: { 
      size: 20,
      weight: "bold"
    }
  };

  var graphic = new Graphic({
    geometry: point,
    symbol: textSymbol
  });

  view.graphics.add(graphic);
}

サンプルコード

以下に全コードを記載します。

  
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
  <title>レイヤ一覧表示</title>
  <link rel="stylesheet" href="https://js.arcgis.com/4.12/esri/css/main.css">
  <script src="https://js.arcgis.com/4.13/"></script>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
    #layerToggle,#sketchToggle{
      display: none;
      top: 80px;
      right: 20px;
      position: absolute;
      z-index: 99;
      background-color: white;
      border-radius: 8px;
      padding: 10px;
      opacity: 0.75;
    }
    #menu{
      padding: 0;
      margin: 0;
      height:42px; 
      background-color:#eeeeee;
      font-weight:bold;
    }
    #menu li {
      height:42px;
      margin-right: 2px;
      display: inline-block;
    }
    #button,#buttonElevation,#buttonSketch,#geometryType {
      left:0px;
      width:164px; 
      height:42px;
    }
    #executeSketch {
      left:0px;
      width:82px; 
      height:42px;
    }

  </style>
  <script>
    require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/VectorTileLayer",
        "esri/layers/FeatureLayer",
        "esri/layers/MapImageLayer",
        "esri/Graphic",
        "esri/views/draw/Draw",
        "dojo/on",
        "dojo/domReady!"
      ],
      function(
        Map, 
        MapView, 
        VectorTileLayer,
        FeatureLayer, 
        MapImageLayer,
        Graphic,
        Draw,
        on
      ) {
        
        var map = new Map({
            basemap: "gray-vector"
        });

        var vectorTileLayer = new VectorTileLayer({
          url:
            "https://www.arcgis.com/sharing/rest/content/items/92c551c9f07b4147846aae273e822714/resources/styles/root.json",
            id:"vectorTile"
        });
        
        var mapImageLayer = new MapImageLayer({
            url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/Dosyasaigai_Tile/MapServer",
            id:"mapImage"
        });

        var boundary = new FeatureLayer({
          url:"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/JPN_Boundaries_ECM/FeatureServer",
          id:"boundary",
          opacity:0.5,
          minScale:1500000,
          maxScale:50000
        });

        map.add(vectorTileLayer);
        map.add(mapImageLayer);
        map.add(boundary);

        var view = new MapView({  
          container: "viewDiv",
          map: map,
          center: [139.740286, 35.678601],
          zoom: 15
        });

        var vectorTileToggle = document.getElementById("vectorTileLayer");
        var mapImageToggle = document.getElementById("mapImageLayer");
        var boundaryToggle = document.getElementById("boundary");

        on(vectorTileToggle, "change", function() {
          vectorTileLayer.visible = vectorTileToggle.checked;
        });
        on(mapImageToggle, "change", function() {
            mapImageLayer.visible = mapImageToggle.checked;
        });
        on(boundaryToggle, "change", function() {
          boundary.visible = boundaryToggle.checked;
        });

        view.on("click", function (event) {  
          if(elevation==1){ 

            var data = { 'lon': event.mapPoint.longitude , 'lat': event.mapPoint.latitude  };
            var querystring = encodeQueryData(data);            
            var url = "http://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" + querystring + "&outtype=JSON"

            // リクエストなげる
            var request = new XMLHttpRequest();
            request.open('GET', url);
            request.onreadystatechange = function () {
                var result = JSON.parse(request.responseText);
                view.popup.open({
                  title: result.elevation,
                  content : result.elevation,
                  location: event.mapPoint 
                });
            };
            request.send(null);
          }
        }); 

        function encodeQueryData(data) {
          let ret = [];
          for (let d in data)
            ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
          return ret.join('&');
        }

        var sketchToggle= document.getElementById("executeSketch");
        on(sketchToggle, "click", function() {
          var geometryType = document.getElementById("geometryType").value;
            
          if(geometryType == "ポイント"){
            enableCreatePoint();
          } else if (geometryType == "ライン"){
            enableCreateLine();
          } else if (geometryType == "ポリゴン"){
            enableCreatePolygon();
          } else if (geometryType == "テキスト"){
            enableCreateText();
          }
        });

        var draw = new Draw({
          view: view
        });

        // ポイント作図
        function enableCreatePoint() {
          var action = draw.create("point");
          action.on("draw-complete", function (evt) {
            createPointGraphic(evt.coordinates);
          });
        }

        function createPointGraphic(coordinates){
          var point = {
            type: "point", 
            x: coordinates[0],
            y: coordinates[1],
            spatialReference: view.spatialReference
          };

          var graphic = new Graphic({
            geometry: point,
            symbol: {
              type: "simple-marker",
              style: "square",
              color: "red",
              size: "16px",
              outline: { 
                color: [255, 255, 0],
                width: 3
              }
            }
          });
          view.graphics.add(graphic);
        }

        // ライン作図
        function enableCreateLine() {
          var action = draw.create("polyline");
            view.focus();
            action.on(
              [
                "vertex-add", // Fキー押下で頂点追加
                "vertex-remove", // Zキー押下で頂点削除
                "cursor-update",
                "draw-complete"
              ],
              createPolylineGraphic
           );
        }

        function createPolylineGraphic(event) {
          var vertices = event.vertices;
          view.graphics.removeAll();

          var graphic = new Graphic({
            geometry: {
              type: "polyline",
              paths: vertices,
              spatialReference: view.spatialReference
            },
            symbol: {
              type: "simple-line",
              color: "red",
              width: 4,
              cap: "round",
              join: "round"
            }
          });

          view.graphics.add(graphic);
        }

        // ポリゴン作図
        function enableCreatePolygon() {
          var action = draw.create("polygon");
            view.focus();
            action.on(
              [
                "vertex-add", // Fキー押下で頂点追加
                "vertex-remove", // Zキー押下で頂点削除
                "cursor-update",
                "draw-complete"
              ],
              createPolygonGraphic
           );
        }

        function createPolygonGraphic(event) {
          var vertices = event.vertices;
          view.graphics.removeAll();

          var graphic = new Graphic({
            geometry: {
              type: "polygon",
              rings: vertices,
              spatialReference: view.spatialReference
            },
            symbol: {
              type: "simple-fill", 
              color: "red",
              style: "solid",
              outline: {  
                color: [255, 255, 0],
                width: 3
              }
            }
          });

          view.graphics.add(graphic);
        }

        // テキスト作図
        function enableCreateText() {
          var action = draw.create("point");
          action.on("draw-complete", function (evt) {
            createTextGraphic(evt.coordinates);
          });
        }

        function createTextGraphic(coordinates){
          var point = {
            type: "point", 
            x: coordinates[0],
            y: coordinates[1],
            spatialReference: view.spatialReference
          };

          var textSymbol = {
            type: "text", 
            color: "red",
            haloColor: "black",
            haloSize: "1px",
            text: "GIS奮闘記",
            xoffset: 3,
            yoffset: 3,
            font: { 
              size: 20,
              weight: "bold"
            }
          };

          var graphic = new Graphic({
            geometry: point,
            symbol: textSymbol
          });

          view.graphics.add(graphic);
        }

      }); 

      function displayLayerList(){
        var toggle = document.getElementById("layerToggle");
        if(toggle.style.display=="block"){
          // 非表示
          toggle.style.display ="none";
        }else{
          // 表示
          toggle.style.display ="block";
        }
      }

      // 標高ボタンを押して標高を取得できるモードを切り替える
      var elevation = 0;
      function getElevation(){
        if(elevation==1){
          elevation = 0;
        }else{
          elevation = 1;
        }
      }
      
      // 作図
      function edit(){
        var toggle = document.getElementById("sketchToggle");
        if(toggle.style.display=="block"){
          // 非表示
          toggle.style.display ="none";
        }else{
          // 表示
          toggle.style.display ="block";
        }
      }  
  </script>
</head>
<body>
    <ul id="menu">
      <li id="menu">
        <button id="button" type="button" onClick="displayLayerList()">レイヤー一覧</button>
      </li>
      <li>
        <button id="buttonElevation" type="button" onClick="getElevation()">標高</button>
      </li>
      <li>
        <button id="buttonSketch" type="button" onClick="edit()">作図</button>
      </li>
    </ul>
  <div id="viewDiv"></div>
  <div id="layerToggle">
    <input type="checkbox" id="vectorTileLayer" checked/>背景<br>
    <input type="checkbox" id="mapImageLayer" checked/>国土数値情報 災害情報<br>
    <input type="checkbox" id="boundary" checked />平成 27 年国勢調査 都道府県界<br>
  </div>
  <div id="sketchToggle">
    <select id="geometryType">
        <option value=""></option>
        <option value="ポイント">ポイント</option>
        <option value="ライン">ライン</option>
        <option value="ポリゴン">ポリゴン</option>
        <option value="テキスト">テキスト</option>
      </select>
    <button id="executeSketch" type="button">実行</button>
  </div>
</body>
</html>

それでは作図してみようと思います。

ポイント

f:id:sanvarie:20191020172615p:plain

ライン

f:id:sanvarie:20191020172720p:plain

ポリゴン

f:id:sanvarie:20191020172818p:plain

テキスト

f:id:sanvarie:20191020172910p:plain

ばっちりですね!ソースはそんなに複雑ではないのに ArcGIS API for JavaScript を使えば本当に色々なことができますね。これからは Web GIS が更に普及していくかと思いますので、もしまだ ArcGIS API for JavaScript を使ったことがないという方は使っておいて損はないかと思います。今回は Draw と Graphic クラスの基本的な使い方を紹介しましたが、今後、機会があればもう一歩踏み込んだ使い方を紹介できればと思います。本日は以上です。

ArcGIS API for JavaScript で標高 API を使ってみよう

さて、本日も ArcGIS API for JavaScript について書いてみようと思います。標高 API は以下のエントリーで紹介しているのですが、Python と C# での呼び出しについてでした。今回は JavaScript を使って処理を書いてみようと思います。

www.gis-py.com

www.gis-py.com

www.gis-py.com

今回やろうとしていること

ArcGIS API for JavaScript を使用して、マップをクリックしたら標高を表示する、というような処理を書いてみようと思います。

今回使用するソース

前回のエントリーをベースに今回使用する処理を書き足そうと思います。

www.gis-py.com

前回の完成イメージです。

f:id:sanvarie:20191012150629p:plain

今回は以下のように「レイヤー一覧」ボタンの隣に「標高」ボタンを追加しようと思います。

f:id:sanvarie:20191019134913p:plain

環境

ArcGIS API for JavaScript 4.12
Chrome

マップクリック時に何か処理を走らせたい場合

View クラスの on() メソッドを使用します。

view.on("click", function(event){
  console.log(event.mapPoint);
});

標高API を使用してみる

以下のように実装してみました。リクエストを投げて受け取ったJSONをパースするだけなので、すごく簡単にできますね。

view.on("click", function (event) {  
  if(elevation==1){ 

    var data = { 'lon': event.mapPoint.longitude , 'lat': event.mapPoint.latitude  };
    var querystring = encodeQueryData(data);            
    var url = "http://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" + querystring + "&outtype=JSON"

    // リクエストなげる
    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.onreadystatechange = function () {
        var result = JSON.parse(request.responseText);
        view.popup.open({
          title: result.elevation,
          content : result.elevation,
          location: event.mapPoint 
        });
    };
    request.send(null);
  }
}); 

function encodeQueryData(data) {
  let ret = [];
  for (let d in data)
    ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
  return ret.join('&');
}

if(elevation==1) の部分ですが、これは「標高」ボタンを押したら標高 API を使用するモードを切り替える処理を以下のように入れているためです。

// 標高ボタンを押して標高を取得できるモードを切り替える
var elevation = 0;
function getElevation(){
  if(elevation==1){
    elevation = 0;
  }else{
    elevation = 1;
  }
}    

HTMLの部分を除いてですが、今回追加した処理はこれだけです。

サンプルコード

全コードを以下に記載します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
  <title>レイヤ一覧表示</title>
  <link rel="stylesheet" href="https://js.arcgis.com/4.12/esri/css/main.css">
  <script src="https://js.arcgis.com/4.12/"></script>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
    #layerToggle {
      display: none;
      top: 80px;
      right: 20px;
      position: absolute;
      z-index: 99;
      background-color: white;
      border-radius: 8px;
      padding: 10px;
      opacity: 0.75;
    }
    #menu{
      padding: 0;
      margin: 0;
      height:42px; 
      background-color:#eeeeee;
      font-weight:bold;
    }
    #menu li {
      height:42px;
      margin-right: 2px;
      display: inline-block;
    }
    #button,#buttonElevation {
      left:0px;
      width:164px; 
      height:42px;
    }

  </style>
  <script>
    require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/VectorTileLayer",
        "esri/layers/FeatureLayer",
        "esri/layers/MapImageLayer",
        "dojo/on",
        "dojo/domReady!"
      ],
      function(
        Map, 
        MapView, 
        VectorTileLayer,
        FeatureLayer, 
        MapImageLayer,
        on
      ) {
        
        var map = new Map({
            basemap: "gray-vector"
        });

        var vectorTileLayer = new VectorTileLayer({
          url:
            "https://www.arcgis.com/sharing/rest/content/items/92c551c9f07b4147846aae273e822714/resources/styles/root.json",
            id:"vectorTile"
        });
        
        var mapImageLayer = new MapImageLayer({
            url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/Dosyasaigai_Tile/MapServer",
            id:"mapImage"
        });

        var boundary = new FeatureLayer({
          url:"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/JPN_Boundaries_ECM/FeatureServer",
          id:"boundary",
          opacity:0.5,
          minScale:1500000,
          maxScale:50000
        });
          
        map.add(vectorTileLayer);
        map.add(mapImageLayer);
        map.add(boundary);

        var view = new MapView({  
          container: "viewDiv",
          map: map,
          center: [139.740286, 35.678601],
          zoom: 15
        });

        var vectorTileToggle = document.getElementById("vectorTileLayer");
        var mapImageToggle = document.getElementById("mapImageLayer");
        var boundaryToggle = document.getElementById("boundary");

        on(vectorTileToggle, "change", function() {
          vectorTileLayer.visible = vectorTileToggle.checked;
        });
        on(mapImageToggle, "change", function() {
            mapImageLayer.visible = mapImageToggle.checked;
        });
        on(boundaryToggle, "change", function() {
          boundary.visible = boundaryToggle.checked;
        });

        view.on("click", function (event) {  
          if(elevation==1){ 

            var data = { 'lon': event.mapPoint.longitude , 'lat': event.mapPoint.latitude  };
            var querystring = encodeQueryData(data);            
            var url = "http://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" + querystring + "&outtype=JSON"

            // リクエストなげる
            var request = new XMLHttpRequest();
            request.open('GET', url);
            request.onreadystatechange = function () {
                var result = JSON.parse(request.responseText);
                view.popup.open({
                  title: result.elevation,
                  content : result.elevation,
                  location: event.mapPoint 
                });
            };
            request.send(null);
          }
        }); 

        function encodeQueryData(data) {
          let ret = [];
          for (let d in data)
            ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
          return ret.join('&');
        }
      }); 

      function displayLayerList(){
          var toggle = document.getElementById("layerToggle");
          if(toggle.style.display=="block"){
            // 非表示
            toggle.style.display ="none";
          }else{
            // 表示
            toggle.style.display ="block";
          }
        }

        // 標高ボタンを押して標高を取得できるモードを切り替える
        var elevation = 0;
        function getElevation(){
          if(elevation==1){
            elevation = 0;
          }else{
            elevation = 1;
          }
        }    
  </script>
</head>
<body>
    <ul id="menu">
      <li id="menu">
        <button id="button" type="button" onClick="displayLayerList()">レイヤー一覧</button>
      </li>
      <li>
        <button id="buttonElevation" type="button" onClick="getElevation()">標高</button>
      </li>
    </ul>
  <div id="viewDiv"></div>
  <div id="layerToggle">
    <input type="checkbox" id="vectorTileLayer" checked/>背景<br>
    <input type="checkbox" id="mapImageLayer" checked/>国土数値情報 災害情報<br>
    <input type="checkbox" id="boundary" checked />平成 27 年国勢調査 都道府県界<br>
  </div>
</body>
</html>

このようにマップをクリックすると標高を取得することができました。皇居周辺は大体標高が20数メートルということがわかりました!

f:id:sanvarie:20191019134517p:plain

今回は簡単にポップアップで標高を表示してみましたが、グラフィックか何かで標高を表示しても面白そうですね。その辺の実装も機会があれば紹介できればと思います。今回は以上です。

ArcGIS API for JavaScript でレイヤーの表示/非表示を行う方法

さて、本日も ArcGIS API for JavaScript について書いてみようと思います。前回は以下エントリーでウィジェットについて紹介したのですが、今回はレイヤー表示の切り替えについて書いてみようと思います。

www.gis-py.com

実行環境

ArcGIS API for JavaScript 4.12
Chrome

今回のポイントとなる処理

  1. レイヤーの表示
  2. レイヤーのスケールフィルタの設定
  3. レイヤーの透明度の設定
  4. 初期表示の設定
  5. レイヤーの表示/非表示の制御
  6. レイヤー一覧ボタンの配置

ざっとこんなところでしょうか。色々あるのですが、どれも難しい処理ではありません。

完成イメージ

初期表示は以下のような感じです。
f:id:sanvarie:20191012120012p:plain

「レイヤー一覧」ボタンを押すとレイヤー一覧が表示されます。
f:id:sanvarie:20191012120037p:plain

レイヤをすべて非表示にした状態です。ベースマップのみが表示されている状態になります。
f:id:sanvarie:20191012120058p:plain

各処理について

各処理のポイントを以下で解説します。

レイヤ-の表示

以下のようにすればレイヤーの表示を行うことができます。var map = new Map({basemap: "gray-vector"}); でマップのオブジェクトを作成します。その後に各レイヤのオブジェクトを作成し、それぞれ map.add(); します。そして、作成したマップをパラメーターにして var view = new MapView({ }); でマップビューのオブジェクトを作成します。

var map = new Map({
    basemap: "gray-vector"
});

var vectorTileLayer = new VectorTileLayer({
    url:
    "https://www.arcgis.com/sharing/rest/content/items/92c551c9f07b4147846aae273e822714/resources/styles/root.json",
    id:"vectorTile"
});

var mapImageLayer = new MapImageLayer({
    url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/Dosyasaigai_Tile/MapServer",
    id:"mapImage"
});

var boundary = new FeatureLayer({
    url:"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/JPN_Boundaries_ECM/FeatureServer",
    id:"boundary",
    opacity:0.5,
    minScale:1500000,
    maxScale:50000
});

map.add(vectorTileLayer);
map.add(mapImageLayer);
map.add(boundary);

var view = new MapView({  
    container: "viewDiv",
    map: map,
    center: [139.740286, 35.678601],
    zoom: 15
});

レイヤーのスケールフィルタの設定

minScale:1500000,
maxScale:50000

レイヤーの透明度の設定

opacity:0.5

初期表示の設定

center: [139.740286, 35.678601],
zoom: 15

レイヤの表示/非表示の制御

dojo/on モジュールを使用します。

on(vectorTileToggle, "change", function() {
    vectorTileLayer.visible = vectorTileToggle.checked;
});
on(mapImageToggle, "change", function() {
    mapImageLayer.visible = mapImageToggle.checked;
});
on(boundaryToggle, "change", function() {
    boundary.visible = boundaryToggle.checked;
});

レイヤー一覧ボタンの配置

当該ボタンを押すと layerToggle の表示/非表示が切り替わります。

function displayLayerList(){
    var toggle = document.getElementById("layerToggle");
    if(toggle.style.display=="block"){
      // 非表示
      toggle.style.display ="none";
    }else{
      // 表示
      toggle.style.display ="block";
    }
}      

<body>
    <ul id="menu">
      <li id="menu">
        <button id="button" type="button" onClick="displayLayerList()">レイヤー一覧</button>
      </li>
    </ul>
  <div id="viewDiv"></div>
  <div id="layerToggle">
    <input type="checkbox" id="vectorTileLayer" checked/>背景<br>
    <input type="checkbox" id="mapImageLayer" checked/>国土数値情報 災害情報<br>
    <input type="checkbox" id="boundary" checked />平成 27 年国勢調査 都道府県界<br>
  </div>
</body>

サンプルコード

全コードを以下に記載します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
  <title>レイヤ一覧表示</title>
  <link rel="stylesheet" href="https://js.arcgis.com/4.12/esri/css/main.css">
  <script src="https://js.arcgis.com/4.12/"></script>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
    #layerToggle {
      display: none;
      top: 80px;
      right: 20px;
      position: absolute;
      z-index: 99;
      background-color: white;
      border-radius: 8px;
      padding: 10px;
      opacity: 0.75;
    }
    #menu{
      padding: 0;
      margin: 0;
	  height:42px; 
      background-color:#eeeeee;
      font-weight:bold;
    }
    #menu li {
      height:42px;
      margin-right: 2px;
      display: inline-block;
    }
    #button {
      left:0px;
      width:164px; 
      height:42px;
    }

  </style>
  <script>
    require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/VectorTileLayer",
        "esri/layers/FeatureLayer",
        "esri/layers/MapImageLayer",
        "dojo/on",
        "dojo/domReady!"
      ],
      function(
        Map, 
        MapView, 
        VectorTileLayer,
        FeatureLayer, 
        MapImageLayer,
        on
      ) {
        
        var map = new Map({
            basemap: "gray-vector"
        });

        var vectorTileLayer = new VectorTileLayer({
          url:
            "https://www.arcgis.com/sharing/rest/content/items/92c551c9f07b4147846aae273e822714/resources/styles/root.json",
            id:"vectorTile"
        });
        
        var mapImageLayer = new MapImageLayer({
            url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/Dosyasaigai_Tile/MapServer",
            id:"mapImage"
        });

        var boundary = new FeatureLayer({
          url:"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/JPN_Boundaries_ECM/FeatureServer",
          id:"boundary",
          opacity:0.5,
          minScale:1500000,
          maxScale:50000
        });
          
        map.add(vectorTileLayer);
        map.add(mapImageLayer);
        map.add(boundary);

        var view = new MapView({  
          container: "viewDiv",
          map: map,
          center: [139.740286, 35.678601],
          zoom: 15
        });

        var vectorTileToggle = document.getElementById("vectorTileLayer");
        var mapImageToggle = document.getElementById("mapImageLayer");
        var boundaryToggle = document.getElementById("boundary");

        on(vectorTileToggle, "change", function() {
          vectorTileLayer.visible = vectorTileToggle.checked;
        });
        on(mapImageToggle, "change", function() {
            mapImageLayer.visible = mapImageToggle.checked;
        });
        on(boundaryToggle, "change", function() {
          boundary.visible = boundaryToggle.checked;
        });
      });

      function displayLayerList(){
        var toggle = document.getElementById("layerToggle");
        if(toggle.style.display=="block"){
          // 非表示
          toggle.style.display ="none";
        }else{
          // 表示
          toggle.style.display ="block";
        }
      }      
  </script>
</head>
<body>
    <ul id="menu">
      <li id="menu">
        <button id="button" type="button" onClick="displayLayerList()">レイヤー一覧</button>
      </li>
    </ul>
  <div id="viewDiv"></div>
  <div id="layerToggle">
    <input type="checkbox" id="vectorTileLayer" checked/>背景<br>
    <input type="checkbox" id="mapImageLayer" checked/>国土数値情報 災害情報<br>
    <input type="checkbox" id="boundary" checked />平成 27 年国勢調査 都道府県界<br>
  </div>
</body>
</html>


少し動かしてみます。以下のようにスケールを変更すると「平成 27 年国勢調査 都道府県界」レイヤーが表示されます。

f:id:sanvarie:20191012122548p:plain

少し見づらいという場合は当該レイヤーを非表示にすれば大丈夫です。「国土数値情報 災害情報」レイヤーがはっきり見えるようになりました。かっこいいですね!

f:id:sanvarie:20191012122719p:plain

ソースは少し複雑そうに見えるのですが、実際は大したことはしていません。単純に地図を表示するだけでも大丈夫なのですが、今回のようにボタンを追加したりすると、そこから色々な処理を展開することができます。例えば、マップとの対話的な操作などでしょうか。次回はこのソースをベースにもう少し機能を追加してみようと思います。本日は以上です。

ArcGIS API for JavaScript のウィジェットを使ってみよう

さて、本日は ArcGIS API for JavaScript について書いてみようと思います。

ArcGIS API for JavaScript とは

ArcGIS API for JavaScript はWeb ブラウザー向けのアプリケーションや Web サイトに GIS(地図)機能を組み込むための API です。これを使用することにより以下のようなことが実現可能になります。

  • 地図表示・操作
  • デバイス連携(GPS・カメラ等)
  • 図形・属性情報編集
  • 距離・面積計測
  • 空間・属性検索
  • 各種解析機能
  • リアルタイム データ表示
  • 3D(※ バージョン 4.0 以降)

なぜ ArcGIS API for JavaScript を取り上げるのか

GIS は昔と違いどんどん Web 化の方向に進んでいます。なので、本ブログでも Web GIS についてもっと取り上げていこうと思い、ArcGIS API for JavaScript をチョイスしてみました。ただ、私も初心者に毛が生えたようなものなので、勉強しながら API にまつわる色々なことを紹介していければと思います。

ArcGIS API for JavaScript は無償で利用可能

開発リソースは ArcGIS for Developers にて開発者アカウントを作成すれば、一部の機能を除き無償で利用可能です。素晴らしいですね!

今回やってみること

まずは簡単なところから始めてみようと思います。色々なことができる API なのですが、私の一押しはウィジェットです。Web アプリなどを開発する際、一から部品を作るのは時間がかかりますし、不具合が発生するリスクもあるかと思います。

ArcGIS API for JavaScript で使用できるウィジェットを使えば、誰でも簡単に便利な部品を使うことができます。今回はその中のほんの一部になりますが、以下を紹介しようと思います。

  • BasemapGallery widget

    • 使用できるベースマップの一覧を表示するウィジェットです。ウィジェット内に表示されているベースマップをクリックすると、マップのベースマップが切り替わります。
  • Locate button

    • 現在位置に移動できるボタンです。モバイルで使用する際に非常に便利な機能ですね。

これらの機能ですが、自分で一から作ろうとすると非常に大変だと思いますが、ArcGIS API for JavaScript を使えば、簡単に実装することができます。

実行環境

ArcGIS API for JavaScript 4.12
Chrome

サンプル

BasemapGallery widget と Locate button を使用したサンプルです。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1, maximum-scale=1,user-scalable=no"
    />
    <title>sample of Locate button and BasemapGallery widget - 4.12</title>
    <link
      rel="stylesheet"
      href="https://js.arcgis.com/4.12/esri/themes/light/main.css"
    />
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>
    <script src="https://js.arcgis.com/4.12/"></script>
    <script>
      require([
        "esri/Map",
        "esri/views/MapView",
        "esri/widgets/Locate",
        "esri/widgets/BasemapGallery"
      ], function(Map, MapView, Locate, BasemapGallery) {
        var map = new Map({
          basemap: "streets"
        });

        var view = new MapView({
          container: "viewDiv",
          map: map,
          center: [139.740286, 35.678601],
          zoom: 18
        });

        var locateBtn = new Locate({
          view: view
        });
        
        var basemapGallery = new BasemapGallery({
          view: view
        });

        // Add the locate widget to the top left corner of the view
        view.ui.add(locateBtn, {
          position: "top-left"
        });
        
        view.ui.add(basemapGallery, {
          position: "top-right"
        });
      });
    </script>
  </head>
  <body>
    <div id="viewDiv"></div>
  </body>
</html>

ポイント

  • <script src="https://js.arcgis.com/4.12/"></script> で API の読み込みをしています。
  • その下の <script>~</script> でマップの表示とウィジェットの読み込みをしています。

これを html として保存して開いてみると、このような感じになります。

f:id:sanvarie:20190930160707p:plain

場所は center: [139.740286, 35.678601] で設定しています。今回は永田町を中心点にしてみました。

マイナスボタンの下にあるのが Locate button で右側にあるのが BasemapGallery widget です。

Locate button を押すと、以下のように現在の場所に移動します。

f:id:sanvarie:20190930161050p:plain

BasemapGallery widget で衛星画像を選択するとこのようにベースマップが切り替わります。素晴らしいですね。

f:id:sanvarie:20190930161355p:plain

このようにウィジェットを使用すると、便利な機能を簡単に実装することができます。今後も他のウィジェットや別の機能などを色々紹介していこうと思うので、興味がある方はぜひ読んでみてください。

参考サイト

ArcGIS API for JavaScript を使用して開発をするために必要となるサンプルコードなどが色々ありますので、ぜひ活用してみてください。

https://www.esrij.com/products/arcgis-api-for-javascript/documents/

Python でリストを CSV 出力する方法

さて、今回は Python でリストを CSV 出力する方法を紹介しようと思います。単純に一次元のリストだけではなく、多次元のリストにも対応したソースを紹介しますので、興味のある方はぜひ読んでみてください。

環境

Windows 10
Python 3.6.5

出力するデータ

すごく単純なデータですが、[1,2,3]と [[1,2,3],[4,5,6],[7,8,9]]というデータを CSV 出力してみようと思います。

使用するライブラリ

csv というPython の標準ライブラリを使用します。

サンプル

一次元と多次元のリストを CSV 出力するサンプルです。

# -*- coding: utf-8 -*-
import csv

def export_list_csv(export_list, csv_dir):

    with open(csv_dir, "w") as f:
        writer = csv.writer(f, lineterminator='\n')

        if isinstance(export_list[0], list): #多次元の場合
            writer.writerows(export_list)

        else:
            writer.writerow(export_list)

def make_list():
    one_dimensional_list = [1,2,3]
    two_dimensional_list = [[1,2,3],[4,5,6],[7,8,9]]

    export_list_csv(one_dimensional_list, r"D:\data\one_dimensional_list.csv")
    export_list_csv(two_dimensional_list, r"D:\data\one_dimensional_list.csv")

if __name__ == '__main__':
    make_list()
  • ポイント
    if isinstance(export_list[0], list): で多次元かどうかを判別しています。意外と便利な isinstance() ですね。

結果

EXCEL で出力した CSV を開いてみました。想定通りの結果が得られました。

f:id:sanvarie:20190928121345p:plain

f:id:sanvarie:20190928121401p:plain

今後は こういった Python の小ネタ集も作っていきたいと思いますので、ぜひ読んでみてください。