GIS奮闘記

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

スポンサーリンク

ArcGIS Pro でレイヤーにフィルターをかける方法

さて、本日は ArcGIS Pro の機能紹介になります。レイヤーを参照している際、特定のフィーチャのみを表示したいということはないでしょうか?例えば、日本地図を表示している際に東京都のフィーチャのみを表示したいといった感じです。ご存知の方も多いかと思うのですが、意外とご存じない方もいらっしゃるので、ここで紹介したいと思います。

対象データ

「日本地図」というレイヤーを使用します。こちらは以下のエントリーでも紹介しているESRIジャパンさんの「全国市区町村界データ」をフィーチャクラスに変換したものです。

www.gis-py.com

f:id:sanvarie:20200115205554p:plain

実行環境

Windows10 64bit
ArcGIS Pro 2.3.3

操作方法

1.「日本地図」レイヤーを右クリックして「プロパティ」を押下
f:id:sanvarie:20200115205000p:plain

2.フィルター設定 > 「新しいフィルター設定」ボタンを押下
f:id:sanvarie:20200115205620p:plain

3.「KEN が 東京都 と等しい」 と設定し、OK。
f:id:sanvarie:20200115205649p:plain

東京都のポリゴンだけが表示されました。
f:id:sanvarie:20200115205358p:plain

まとめ

レイヤーのフィルター設定はこのように簡単に行うことができます。状況に応じて、必要なフィーチャの表示/非表示を切り替えることによって、より効率的に作業を進めることができるようになるかと思います。

本日は以上です。

Python で SQLite を使ってみよう!

明けましておめでとうございます。少し更新が止まっていましたが、また再開しようと思います。

去年は私にとっては色々変化の大きい年でした。自分の勉強不足なども痛感し、今年はより一層努力しなければと思います。

さて、本日は Python で SQLite を使ってみようと思います。GIS とはあまり関係ないと思われるかもしれませんが、GIS データを扱うにあたってデータベースは避けては通れない道かと思います。今回だけではなく、今後もデータベースについて色々紹介できればと思います。

SQLite とは

SQLiteは、パブリックドメインの軽量な関係データベース管理システム のことですね(SQLite - Wikipedia)。簡単に言うと、SQLSERVER や Oracle などと異なり、ファイルベースで簡単に扱えるデータベース、といった感じでしょうか。

使用するライブラリ

Python 標準ライブラリである sqlite3 を使用します。

SQLite のGUI 管理ツール

SQLite のGUI 管理ツールですが、DB Browser for SQLite を使用します。

f:id:sanvarie:20200113100352p:plain

関連エントリー

以下のエントリーで Python で SQLSERVER の操作について紹介していますので、興味がありましたらぜひ読んでみてください。

www.gis-py.com

環境

Windows10 64bit
Python 3.6.6
DB Browser for SQLite 3.11.2

sqlite3 の使用方法

sqlite3 を使用して以下のような操作をするための方法を書いてみました。

  1. データベースの作成/接続
  2. テーブルの作成
  3. 挿入
  4. 更新
  5. 削除
  6. 検索

データベースの作成/接続

対象のSQLiteのファイル(sample_data.db)がなかったら作成して、あったら接続します。

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

# sample_data.db がなかったら作成、あったら接続
db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)
conn.close()

実行前

f:id:sanvarie:20200113102206p:plain

実行後

f:id:sanvarie:20200113102229p:plain

sample_data.db が作成されたことがわかります。sample_data.db を DB Browser for SQLite で読み込んだ結果が以下になります。

f:id:sanvarie:20200113102352p:plain

テーブルの作成

「SAMPLE」というテーブルを作成するサンプルです。コネクションを取得して、作成した SQL を実行するだけですね。

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

db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)

def create_table():
    sql = '''CREATE TABLE IF NOT EXISTS SAMPLE
                 (ID    INTEGER PRIMARY KEY,
                  TEST1 TEXT,
                  TEST2 TEXT,
                  TEST3 TEXT)'''

    conn.execute(sql)
    conn.commit()
    conn.close()

create_table()

以下のような結果になりました。カラムもしっかり作成されていることがわかります。 f:id:sanvarie:20200113103215p:plain

挿入

テーブルの作成と基本的には一緒で SQL をINSERT 文にするだけですね。

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

db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)

def insert_record():
    sql = "INSERT INTO SAMPLE VALUES (1, 'あ', 'い', 'う')"
    conn.execute(sql)

    sql = "INSERT INTO SAMPLE VALUES (2, 'てすと1', 'てすと2', 'てすと3')"
    conn.execute(sql)

    sql = "INSERT INTO SAMPLE VALUES (3, '12345', '22222', '55555')"
    conn.execute(sql)

    conn.commit()
    conn.close()

insert_record()

想定通りの結果になりました。 f:id:sanvarie:20200113125244p:plain

更新

こちらも SQL を UPDATE 文に変えただけですね。

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

db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)

def update_record():
    sql = "UPDATE SAMPLE SET TEST1 = 'か' WHERE ID = 1"
    conn.execute(sql)
    conn.commit()
    conn.close()

update_record()

しっかり UPDATE されていることがわかります。 f:id:sanvarie:20200113125551p:plain

削除

こちらも SQL を DELETE 文に変えただけですね。

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

db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)

def delete_record():
    sql = "DELETE FROM SAMPLE WHERE ID = 3"
    conn.execute(sql)
    conn.commit()
    conn.close()

delete_record()

ID = 3 のレコードが削除されていることがわかります。 f:id:sanvarie:20200113125731p:plain

検索

最後に検索ですが、こちらは他のとは少し異なりますのでご注意ください。 SQL を SELECT 文に変えて、そのあと execute した結果をループします。

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

db_path = "D:\data\SQLite\sample_data.db"
conn = sqlite3.connect(db_path)

def select_record():
    sql = "SELECT * FROM SAMPLE WHERE ID = 2"
    for row in conn.execute(sql):
        print(row)

select_record()

このように SELECT した結果を取得することができます。 f:id:sanvarie:20200113130200p:plain

Python での SQLite の操作について一通り紹介しましたが、すごく簡単にできることがわかりました。私は業務で SQLite はあまり使ったことがないのですが、おそらく世間的にはたくさん使われているデータベースだと思います。やはりファイルベースで扱えるというメリットが人気の秘密でしょうか。

本日は以上です。

GeoPandas、Fiona、Shapely を 使ってファイルジオデータベース内のデータを Shape 出力する方法

さて、本日は GeoPandas、Fiona、Shapely を 使ってファイルジオデータベース内のデータを Shape 出力してみようと思います。どれも GIS 界では有名でとても優秀なライブラリですね。これらについては過去のエントリーで紹介しているので、詳しくはそちらを読んでみてください。

www.gis-py.com

www.gis-py.com

www.gis-py.com

実行環境

実はGeoPandas、Fiona、Shapely 以外にも Pandas を使います。ただ、Pandas は Shape 出力には関係ないところで使用します。詳細は後述します。

Windows 10
Python 3.6.6
ArcGIS Pro 2.4
GeoPandas 0.6.1
Fiona 1.8.4
Shapely 1.6.4
Pandas 0.25.2

出力するデータ

このようなデータをフィーチャクラス単位で出力してみようと思います。フィーチャデータセットも含めてみました。また、アノテーションが入っていますが、アノテーションは出力対象外になるように制御をかけようと思います。

f:id:sanvarie:20191030174559p:plain

一部ではありますが、データはこのように日本地図のポリゴン、各県の外郭線のライン、横須賀市内に作成したポイントなどがあります。

f:id:sanvarie:20191030175020p:plain

f:id:sanvarie:20191030175047p:plain

属性には日本語データが入っています。 f:id:sanvarie:20191030175507p:plain

f:id:sanvarie:20191030175532p:plain

今回直面した問題

最初はこんなにたくさんのライブラリを使うつもりはなく、単純に GDAL とかで出力しようかと思ったのですが、日本語データが含まれているとうまく出力できず、また、GeoPandas や Fiona でも同じように試したのですが、日本語データがネックになり断念しました。もしかしたら何かやり方があるのかもしれないのですが、とりあえず今回は別のアプローチをとりました。ちなみに日本語データが含まれていなければ以下のような感じにすれば大丈夫かと思います(GeoPandas と Fiona を使用した例)。

import fiona
import geopandas as gpd
import os

gdb = r"D:\python\data\Sample.gdb"
layers = fiona.listlayers(gdb)
anno_flg = 0

for layer in layers:

    gdf = gpd.read_file(gdb, driver="FileGDB", layer=layer)

    for column in gdf.columns:

        if column == "AnnotationClassID":# アノテーションは対象外
            anno_flg = 1

        if column == "Shape_Length":
            gdf.drop('Shape_Length', axis=1, inplace=True)

        if column == "Shape_Area":
            gdf.drop('Shape_Area', axis=1, inplace=True)

    if anno_flg == 1:
        anno_flg = 0
        continue

    out = os.path.join(r"D:\python\data\shape", layer + ".shp")
    gdf.to_file(out)

直面した問題を回避するために

出力するファイルを以下の二つに分けました。
1.Shape・・・図形データを出力
2.CSV・・・属性データを出力

このようにファイルを分けて出力すれば日本語データが含まれていても問題ないかと思います。双方にキーとなるカラムも含めていますので、データ出力後はそのキーを使って図形と属性を紐づけるだけですね。Pandas は CSV 出力の処理で使用します。

サンプルコード

import os
import geopandas as gpd
import fiona
from shapely.geometry import Point, LineString, Polygon, MultiPolygon, MultiLineString, MultiPoint
import pandas as pd

def make_shape_file():
    gdb = r"D:\python\data\ToShapeFile.gdb" # 出力対象のGDB
    layers = fiona.listlayers(gdb)

    for layer in layers:
        new_data = gpd.GeoDataFrame()
        new_data['geometry'] = None

        gdf = gpd.read_file(gdb, driver="FileGDB", layer=layer)

        if 'AnnotationClassID' in gdf[0:1].columns: # アノテーションは処理対象外
            continue

        geometry_type = gdf[0:1]['geometry'].geom_type[0]

        for index, row in gdf.iterrows():

            if geometry_type == "MultiPolygon":
                new_data.loc[index, 'geometry'] = MultiPolygon(row.geometry)

            elif geometry_type == "Polygon": # 今回は不使用
                new_data.loc[index, 'geometry'] = Polygon(row.geometry)

            elif geometry_type == "LineString": # 今回は不使用
                new_data.loc[index, 'geometry'] = LineString(row.geometry)

            elif geometry_type == "MultiLineString":
                new_data.loc[index, 'geometry'] = MultiLineString(row.geometry)

            elif geometry_type == "Point":
                new_data.loc[index, 'geometry'] = Point(row.geometry)

            elif geometry_type == "MultiPoint": # 今回は不使用
                new_data.loc[index, 'geometry'] = MultiPoint(row.geometry)

        # Shapeファイル出力
        out = os.path.join(r"D:\python\data\shape", layer + ".shp")
        new_data.to_file(out)

        # CSVファイル出力
        df = pd.DataFrame(gdf)
        df.to_csv(os.path.join(r"D:\python\data\csv", layer + ".csv"))

make_shape_file()

結果

Shape も CSV もきちんと出力されましたね。

f:id:sanvarie:20191030181847p:plain

f:id:sanvarie:20191030181816p:plain

Shape を ArcGIS Pro で読み込むと図形データのみ出力されていることがわかります。

f:id:sanvarie:20191030182008p:plain

CSV はこのようになっています。左端の列がキーです。

f:id:sanvarie:20191030182422p:plain

色々苦労はしたものの、プログラム自体はけっこうあっさりしてますね。本当に便利なライブラリ達です。次は GDAL で同じことを実現させてみたいですね。やり方がごっそりと変わると思いますが面白そうです。

今回は出力するファイルを Shape と CSV に分ける方法をとったのですが、もし Shape ファイルを出力するだけで問題ないやり方をご存知の方がいましたらコメントいただけるととても助かります。よろしくお願いします。本日は以上です。

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

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