QGISプラグイン開発の勉強(2)
概要
- 前回(https://anorith.hatenablog.com/entry/2023/10/28/234120)の続き
- プラグインのアイコン・ショートカット設定
- 画像を保存する処理の追加
参考
- ChatGPT
- GPT-4を使用しました。
- 記事の最後に少しコメントを記載しています。
アイコン設定とショートカット設定
前回のプラグインフォルダをそのまま使用します。フォルダにアイコン用の画像を格納します。
アイコンには以下の画像を使用しました。DALL·E3に白背景のアイコンを描いてもらい、ノイズ除去のためvectorizer.aiで一度ベクター化&pngで再保存して、白背景部分をPythonで透過に処理してアイコンにしました(AIに頼りすぎでは・・・?)。
まずインポート部分にアイコン設定用のモジュールを追加します。
from PyQt5.QtWidgets import QAction from PyQt5.QtGui import QIcon
次に__init__.py
のinitGui
の箇所を以下のように書き換えます。
def initGui(self): self.action = QAction('MyPluginAction', self.iface.mainWindow()) icon_path = os.path.join(os.path.dirname(__file__), "icon.png") self.action.setIcon(QIcon(icon_path)) self.action.setShortcut("Ctrl+T") self.action.triggered.connect(self.run) self.iface.addToolBarIcon(self.action)
以下のようにアイコンが反映されました。オリジナルのアイコンだと一気にプラグイン作ってる感じが出ますね。
またショートカットCtrl+T
も使用してrun
の処理が行えるようになっています。
画像保存プラグインの作成
インポート部分に必要なモジュールを追加します。
import os from PyQt5.QtWidgets import QAction from PyQt5.QtGui import QIcon from qgis.core import QgsProject, QgsMapRendererParallelJob
run
の中身を以下の内容をに書き換えます(ChatGPTの出力そのままです)。
def run(self): # 現在のプロジェクトのファイルパスを取得 project_path = QgsProject.instance().fileName() if not project_path: # プロジェクトが保存されていない場合は、処理を中止 return # プロジェクトのディレクトリパスを取得 project_directory = os.path.dirname(project_path) image_path = os.path.join(project_directory, "map_image.png") # 現在の地図の設定を取得 map_settings = self.iface.mapCanvas().mapSettings() # レンダリングジョブを作成して地図を画像として保存 job = QgsMapRendererParallelJob(map_settings) job.start() job.waitForFinished() image = job.renderedImage() image.save(image_path)
以下の状態でプラグインを実行します(アイコンクリックorCtrl+T
)。
以下の画像がQGISプロジェクトと同じ箇所に保存されます。
思ったより簡単にできました。 画像保存の話は以前も書きましたが、こういう書き方もあるのだと勉強になりました(以前の記事:https://anorith.hatenablog.com/entry/2023/01/15/213632)。
ChatGPTについて
今回はChatGPT使って勉強してみましたが、ぶっちゃけブログ記事書く(見る)よりChatGPTに聞いた方がよくない?という気がしてきますね・・・。
チャットのログ:https://chat.openai.com/share/c5fea02b-6009-4b05-8317-bc11d9b643e7
ログの途中のショートカットのやりとりは別原因だったためスルーして下さい。
QGISプラグイン開発の勉強(1)
概要
参考
- qgis-minimal-plugin
- https://github.com/wonder-sk/qgis-minimal-plugin
- 最小限のプラグインに必要なものが記載されている。
- ドキュメント
バージョン情報
- QGISのバージョン: 3.22.14
プラグイン作成
最小構成のQGISプラグインを作ってプラグイン開発の勉強をしてみました。プラグイン作成は初めてです。上記の「qgis-minimal-plugin」を使用しました。
ネット上だとPlugin Builderを使った手順は多いですが、とりあえず触ってみる、という感じだけならこっちでも十分そうです(というか最初に手を付けるならこっちの方がいい気がします)。
とりあえずReadmeの手順に従ってプラグインフォルダの中にminimal
フォルダを作成し、その中にmetadata.txt
と__init__.py
を作成します。
プラグインフォルダ:C:\Users\USER\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\minimal
両方中身もコピペしますが、__init__.py
はここにも書いておきます。
from PyQt5.QtWidgets import QAction, QMessageBox def classFactory(iface): return MinimalPlugin(iface) class MinimalPlugin: def __init__(self, iface): self.iface = iface def initGui(self): self.action = QAction('Go!', self.iface.mainWindow()) self.action.triggered.connect(self.run) self.iface.addToolBarIcon(self.action) def unload(self): self.iface.removeToolBarIcon(self.action) del self.action def run(self): QMessageBox.information(None, 'Minimal plugin', 'Do something useful here')
classFactory
は__init__.py
に必須のクラスのようですね。__init__
,initGui
,unload
がプラグインクラスに必須のメソッドの様です。initGui
はプラグインが有効になったときに呼ばれるメソッドで、unload
はプラグインが無効になったときに呼ばれるメソッドの様です。- どうやらここでは
Go!
という名前(テキスト属性)でアクションを作成し、メッセージを表示するメソッドを紐づけてツールバーに追加するようです。
有効化してみます。
↓追加されました。アクションがプラグインツールバーに追加されたようです。
アクションをクリックすると以下のメッセージが表示されます。
とりあえず動作確認はできました。
変更の反映
自分でコードをいじって遊びたいのでコードの変更の反映方法を確認しておきます。
とりあえず「プラグインの管理とコンソール」で無効にして再度有効化すればよいようです。
initGUI
の文字をGo?
にしてみます。
def initGui(self): self.action = QAction('Go?', self.iface.mainWindow()) self.action.triggered.connect(self.run) self.iface.addToolBarIcon(self.action)
反映されました。
今のところQGISらしさが皆無なので、QGISのPythonAPIも使用してみます。 プラグイン開発2 · GIS実習オープン教材を参考にレイヤ数を表示してみます。
run
の中身を以下のように変更して再度読み込みます。
def run(self): cnt = self.iface.mapCanvas().layerCount() QMessageBox.information(None, 'Minimal plugin', f'layers: {cnt}')
レイヤ数が表示されました。
これだけで基本的なプラグインとして動かせるので色々遊べそうですね。
WSL2のDockerでmastodonを起動してみる(HTTPS)
概要
- 前回(https://anorith.hatenablog.com/entry/2023/08/13/232556)のHTTPS対応版
- HTTPS対応は自己署名証明書とnginx(Docker不使用)で対応
手順(メモ)
前回のブログ内容と以下リンクを参考にやりました。
https://gist.github.com/melroy89/6fe7d05bdc0cfd2153b77310abf62990
特に詰まることろはなかったので、メモ書き程度の内容を記載しておきます。
- DBの準備
上記リンクの
docker-compose run --rm web bundle exec rails db:setup
の箇所は代わりに前回記事のマイグレーションのコマンドを実行した(違いは不明)。
- 証明書の作成
証明書の作成はWSL上で以下のコマンドで実行。
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx/ssl/server.key -out nginx/ssl/server.crt
参考にしたサイトがあったが忘れた(どこでも出てくるレベルのコマンドっぽいので実際に参考にしたサイトを後から探せなかった・・・)。
- WSLのネットワークについて
WSLを再インストールした影響か、WSLから外部のネットワークへつながらなくなった。以下リンクを参考にしたところ解決した(mastodonの設定とは全く関係ない)。 - https://qiita.com/RIckyBan/items/87b558220d2bc5dd11cb
その他
HTTPS対応したことでアイコンや画像もちゃんと表示されるようになりました。
※画像はnijijourneyで生成したもの
WSL2のDockerでmastodonを起動してみる(HTTP)
概要
手順
ソースコードのDL
作業用に適当なディレクトリを作成して作業します。
まずはGitHubからソースのダウンロードを行います。 この時点の最新の安定版である「stable-4.1」を使用します。
wget https://github.com/mastodon/mastodon/archive/refs/heads/stable-4.1.zip unzip stable-4.1.zip cd mastodon-stable-4.1
※リポジトリごとDLしていますが、実際に使用するのは「.env.production.sample」ファイルと「docker-compose.yml」くらいです。
docker-compose.ymlの編集
dbの環境変数を「POSTGRES_USER: mastodon」「POSTGRES_DB: mastodon」「POSTGRES_PASSWORD: password」としてユーザーを設定します(ついでに「POSTGRES_HOST_AUTH_METHOD」も書き方を統一&「healthcheck」もちょっと修正)。
db: restart: always image: postgres:14-alpine shm_size: 256mb networks: - internal_network healthcheck: # test: ['CMD', 'pg_isready', '-U', 'postgres'] test: ['CMD', 'pg_isready', '-U', 'mastodon'] volumes: - ./postgres14:/var/lib/postgresql/data environment: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_USER: mastodon POSTGRES_DB: mastodon POSTGRES_PASSWORD: password
HTTP通信をするためにコンテナ内部の設定ファイルを変更したいので「web」の実行時のコマンドに以下の内容を追加します。
sed 's/config.force_ssl = true/config.force_ssl = false/' /opt/mastodon/config/environments/production.rb > /opt/mastodon/config/environments/production.rb
また、環境変数にもHTTP通信用の設定を追加します。
environment: HTTPS_LOCAL: 'false'
また、Dockerイメージのビルドを省略するために「build .」をコメントアウトします。「web」「streaming」「sidekiq」の3か所あります。
最終的に以下のようなdocker-composeファイルになります。
docker-compose.yml
version: '3' services: db: restart: always image: postgres:14-alpine shm_size: 256mb networks: - internal_network healthcheck: # test: ['CMD', 'pg_isready', '-U', 'postgres'] test: ['CMD', 'pg_isready', '-U', 'mastodon'] volumes: - ./postgres14:/var/lib/postgresql/data environment: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_USER: mastodon POSTGRES_DB: mastodon POSTGRES_PASSWORD: password redis: restart: always image: redis:7-alpine networks: - internal_network healthcheck: test: ['CMD', 'redis-cli', 'ping'] volumes: - ./redis:/data # es: # restart: always # image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4 # environment: # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true" # - "xpack.license.self_generated.type=basic" # - "xpack.security.enabled=false" # - "xpack.watcher.enabled=false" # - "xpack.graph.enabled=false" # - "xpack.ml.enabled=false" # - "bootstrap.memory_lock=true" # - "cluster.name=es-mastodon" # - "discovery.type=single-node" # - "thread_pool.write.queue_size=1000" # networks: # - external_network # - internal_network # healthcheck: # test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"] # volumes: # - ./elasticsearch:/usr/share/elasticsearch/data # ulimits: # memlock: # soft: -1 # hard: -1 # nofile: # soft: 65536 # hard: 65536 # ports: # - '127.0.0.1:9200:9200' web: # build: . image: ghcr.io/mastodon/mastodon:v4.1.6 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; sed 's/config.force_ssl = true/config.force_ssl = false/' /opt/mastodon/config/environments/production.rb > /opt/mastodon/config/environments/production.rb; bundle exec rails s -p 3000" networks: - external_network - internal_network healthcheck: # prettier-ignore test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1'] ports: - '127.0.0.1:3000:3000' depends_on: - db - redis # - es volumes: - ./public/system:/mastodon/public/system environment: HTTPS_LOCAL: 'false' streaming: # build: . image: ghcr.io/mastodon/mastodon:v4.1.6 restart: always env_file: .env.production command: node ./streaming networks: - external_network - internal_network healthcheck: # prettier-ignore test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'] ports: - '127.0.0.1:4000:4000' depends_on: - db - redis sidekiq: # build: . image: ghcr.io/mastodon/mastodon:v4.1.6 restart: always env_file: .env.production command: bundle exec sidekiq depends_on: - db - redis networks: - external_network - internal_network volumes: - ./public/system:/mastodon/public/system healthcheck: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] ## Uncomment to enable federation with tor instances along with adding the following ENV variables ## http_proxy=http://privoxy:8118 ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true # tor: # image: sirboops/tor # networks: # - external_network # - internal_network # # privoxy: # image: sirboops/privoxy # volumes: # - ./priv-config:/opt/config # networks: # - external_network # - internal_network networks: external_network: internal_network: internal: true
.env.productionの作成、設定
「.env.production.sample」をコピーして「.env.production」を作成します。
cp .env.production.sample .env.production
このファイルの設定に鍵の生成が必要なのですが、今回は公式とは別のlinuxserverが作成しているコンテナを使用します(生成さえできれば手段はなんでもよいので)。
「SECRET_KEY_BASE」「OTP_SECRET」に設定する値を生成するために以下のDockerイメージとコマンドを使用します。2回実行し表示された値をそれぞれ「SECRET_KEY_BASE」「OTP_SECRET」に設定します。
docker run --rm -it --entrypoint /bin/bash lscr.io/linuxserver/mastodon generate-secret
「VAPID_PRIVATE_KEY」「VAPID_PUBLIC_KEY」に設定する値を生成するために以下のコマンドを使用します。こちらはそのままコピペできる形で出力されます。
docker run --rm -it --entrypoint /bin/bash lscr.io/linuxserver/mastodon generate-vapid
その他以下のように設定します。
LOCAL_DOMAIN=localhost REDIS_HOST=redis DB_HOST=db DB_USER=mastodon DB_NAME=mastodon DB_PASS=password ES_ENABLED=false S3_ENABLED=false
最終的には以下のようなファイルになります。
.env.production
# This is a sample configuration file. You can generate your configuration # with the `rake mastodon:setup` interactive setup wizard, but to customize # your setup even further, you'll need to edit it manually. This sample does # not demonstrate all available configuration options. Please look at # https://docs.joinmastodon.org/admin/config/ for the full documentation. # Note that this file accepts slightly different syntax depending on whether # you are using `docker-compose` or not. In particular, if you use # `docker-compose`, the value of each declared variable will be taken verbatim, # including surrounding quotes. # See: https://github.com/mastodon/mastodon/issues/16895 # Federation # ---------- # This identifies your server and cannot be changed safely later # ---------- LOCAL_DOMAIN=localhost # Redis # ----- REDIS_HOST=redis REDIS_PORT=6379 # PostgreSQL # ---------- DB_HOST=db DB_USER=mastodon DB_NAME=mastodon DB_PASS=password DB_PORT=5432 # Elasticsearch (optional) # ------------------------ ES_ENABLED=false ES_HOST=localhost ES_PORT=9200 # Authentication for ES (optional) ES_USER=elastic ES_PASS=password # Secrets # ------- # Make sure to use `rake secret` to generate secrets # ------- SECRET_KEY_BASE=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX OTP_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # Web Push # -------- # Generate with `rake mastodon:webpush:generate_vapid_key` # -------- VAPID_PRIVATE_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX VAPID_PUBLIC_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # Sending mail # ------------ SMTP_SERVER= SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= SMTP_FROM_ADDRESS=notifications@example.com # File storage (optional) # ----------------------- S3_ENABLED=false S3_BUCKET=files.example.com AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml # to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800). # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952
DBの設定とアセットのコンパイル
以下のコマンドでDBのマイグレーションを行います。大量のログが出力されるので少し待ちます。
docker-compose run --rm web rails db:migrate
次にアセットのコンパイルを行います(作成済みのDockerイメージを使用する場合は不要という情報もありましたが念のため・・・)。
docker-compose run --rm web rails assets:precompile
コンテナの起動
以下コマンドでコンテナを起動します。
docker-compose up -d
以下のようなログが出ます。
mastodon-stable-41_db_1 is up-to-date mastodon-stable-41_redis_1 is up-to-date Creating mastodon-stable-41_web_1 ... done Creating mastodon-stable-41_streaming_1 ... done Creating mastodon-stable-41_sidekiq_1 ... done
「docker ps」でコンテナを確認すると以下のようになっています(直後なのでまだhealth: startingですが・・・)。
c7f09f791613 ghcr.io/mastodon/mastodon:v4.1.6 "/usr/bin/tini -- no…" 24 seconds ago Up 21 seconds (health: starting) 3000/tcp, 127.0.0.1:4000->4000/tcp mastodon-stable-41_streaming_1 abfb7420b861 ghcr.io/mastodon/mastodon:v4.1.6 "/usr/bin/tini -- bu…" 24 seconds ago Up 20 seconds (health: starting) 3000/tcp, 4000/tcp mastodon-stable-41_sidekiq_1 911368ee133a ghcr.io/mastodon/mastodon:v4.1.6 "/usr/bin/tini -- ba…" 24 seconds ago Up 20 seconds (health: starting) 127.0.0.1:3000->3000/tcp, 4000/tcp mastodon-stable-41_web_1 ee657ada8db7 postgres:14-alpine "docker-entrypoint.s…" About a minute ago Up About a minute (healthy) mastodon-stable-41_db_1 d982b142e1a4 redis:7-alpine "docker-entrypoint.s…" About a minute ago Up About a minute (healthy) mastodon-stable-41_redis_1
ここで「http://localhost:3000/」にアクセスするとmastodonの画面が表示されます。
※普通に表示されることもありますが、chromeの設定やキャッシュによってHTTPSにリダイレクトされ「ERR_SSL_PROTOCOL_ERROR」が出ることもあります。その場合はシークレットモードだとアクセスできます。
ユーザーの作成とログイン
このままだとログインできないのでユーザーを作成します。メールサーバーを設定しておらず通常のようにユーザーの新規作成ができないのでコンテナに入って無理やり作成します。
以下のコマンドでコンテナに入ります。
docker exec -it mastodon-stable-41_web_1 /bin/bash
コンテナ内で以下のコマンドでユーザーを作成します。「confirmed 」はメール認証を省略するためのオプションです。
bundle exec bin/tootctl accounts create admin_user --email admin@localhost --confirmed --role Admin
※メアドは適当です。example.comだとはじかれたのでlocalhostにしています。ついでにAdmin権限を与えていますが、あまり意味はないです。
パスワードが表示されるので上のメアドと表示されたパスワードを使用してログインします。
ログインすると以下のような画面になります。
投稿もできます。
ただしアイコン画像などはリンクがhttpsで設定されているためか表示されません。 httpで確認できるのはこの程度っぽいですね。
その他コメント
特にHTTPS通信を回避するところでめちゃくちゃ詰まりました。最初はオレオレ証明書でやろうとしたりもしてましたが、ごちゃごちゃしてきたので一旦http通信で起動する方針にしました。最終的には比較的するっといけましたが、途中無駄にDockerのネットワークの仕様など調べて疲れました・・・。https版の設定もできたら記事にしようと思います。
参考
- Dockerで雑にMastodonを起動する方法
- https://qiita.com/zembutsu/items/fd52a504321dd5d6f0b8
- わりとベースにしているかもしれない記事。mastodonのバージョンが古いのでいろいろ異なる。
- mastodon コマンド備忘録
- linuxserver/docker-mastodon(GitHub)
- Google Chrome redirecting localhost to https
- https://stackoverflow.com/questions/25277457/google-chrome-redirecting-localhost-to-https
- Chromeでlocalhostが見れない場合の参考?ぶっちゃけいろいろ試しても直らなった(記事中の画像はChrome内の別プロファイルで表示した)。
- Cannot find user role with that name
登記所備付地図データをGeoPackageにしてQGISで読み込む
概要
参考
- 法務省登記所備付地図データ(G空間情報センター)
- mojxml2geojson(GitHub)
- https://github.com/JDA-DM/mojxml2geojson
- 配布されているxmlをgeojsonに変換するPythonライブラリ。
- デジタル庁の仕事っぽい。
- 法務省地図XMLアダプトプロジェクト
- https://github.com/amx-project
- 今回は使用していないが、DLするのはこっちの方が楽そう。
- ベクトルタイルも配布してるっぽい:https://github.com/amx-project/a-spec
DLしたxmlをgeojsonに変換
公式からデータをDLする。
Python環境にmojxml2geojsonをインストールして、以下のスクリプトでDLしたzipを解凍してgeojsonに変換する。 (ブログに載せるほどのスクリプトでもないが・・・)
import os import glob import shutil import subprocess input_file = R"D:\Geo\touki\47201-3600.zip" out_dir = R"D:\Geo\touki_out" # DLしたzipを解凍 input_name = os.path.basename(input_file).split(".")[0] temp_dir = os.path.join(os.path.dirname(input_file), input_name) shutil.unpack_archive(input_file, temp_dir) # 中身のzipを解凍 zip_path_list = glob.glob(os.path.join(temp_dir, "*.zip")) xml_dir = os.path.join(out_dir, "xml", input_name) for zip_path in zip_path_list: shutil.unpack_archive(zip_path, xml_dir) # xmlをgeojsonに変換 xml_path_list =glob.glob(os.path.join(xml_dir, "*.xml")) for idx, xml_path in enumerate(xml_path_list): print(f"[{idx+1}/{len(xml_path_list)}] {os.path.basename(xml_path)}") cmd = ["mojxml2geojson", xml_path] subprocess.run(cmd)
- xml -> geojsonの変換は結構遅い。大量にやるなら並列化したいところ。
geopandasでgeojsonをGeoPackageに変換
以下スクリプトでDLしたzipの単位でGeoPackageにまとめて保存する。
import os import glob import shutil import geopandas as gpd import pandas as pd input_dir = R"D:\Geo\touki_out\xml\47201-3600" # 先ほどgeojsonに変換したディレクトリを指定 out_dir = R"D:\Geo\touki_out\gpkg" os.makedirs(out_dir, exist_ok=True) input_name = os.path.basename(input_dir) geojson_path_list = glob.glob(os.path.join(input_dir, "*.geojson")) gdf_list = [] for idx, geojson_path in enumerate(geojson_path_list): print(f"[{idx+1}/{len(geojson_path_list)}] {os.path.basename(geojson_path)}") gdf = gpd.read_file(geojson_path) gdf = gdf[gdf["座標系"] != "任意座標系"] gdf_list.append(gdf.to_crs("epsg:4326")) gdf = pd.concat(gdf_list) out_path = os.path.join(out_dir, f"{input_name}.gpkg") gdf.to_file(out_path, driver='GPKG', layer=input_name)
- 大したデータ量でもないのでGeoDataFrameの時点でまとめて一つのGeoDataFrameに してしまっている。大量に処理するなら小分けしてGeoPackageへの保存は追記モードでやった方がよさげ。
- 任意座標系はあきらめる。
.to_crs("epsg:4326")
をやっておかないと以下のようなエラーが出る場合がある。
ValueError: Cannot determine common CRS for concatenation inputs, got ['JGD2011', 'WGS 84']. Use `to_crs()` to transform geometries to the same CRS before merging.
- 本当に少ししかやっていないので、なんかほかにも例外出るようなデータはあるかも・・・。
QGISで表示してみる
大字コードで色分けすると以下のような感じ。
QGISのフィルタでNOT "地番" LIKE '%地区外%'
として地区外のデータは除外して表示している。xml -> geojsonの段階でmojxml2geojson
の実行オプションとして--exclude
としておけばそもそも地区外のデータをgeojsonに含まれないようにもできる。
フィルタに関するよさげなリンクメモ。
QGIS(1) pyQGISで画像を出力する
概要
- pyQGISで画像を出力して保存する
参考
- PyQGIS 開発者用 Cookbook 地図のレンダリングと印刷
- PyQGIS: Render (Print/Save) a Layer as an Image
- https://opensourceoptions.com/blog/pyqgis-render-print-save-a-layer-as-an-image/
- 似たような記事。
QPainter
というものを使っている。よくわからない。extent
の指定の仕方が違うので参考になるかも。
- pyQGISでできることのまとめ(自分用)
- https://qiita.com/nishi_bayashi/items/96e6eff8bf4fd578c32d
- 今回の内容とはあまり関係ないけど便利そうなのでメモとして記載しておく
使用するデータなど
- 指定緊急避難場所データ-静岡県
- https://www.geospatial.jp/ckan/dataset/hinanbasho-22
- geojsonを使用(shapeでもよいはず)
- QGISのバージョン: 3.22.14
QGISで表示されている内容の画像化
QGISで上記のデータとOSMを読み込みます。 避難場所データは「洪水」属性で分類して色分けして表示しています。 ここでのスタイルが画像にも反映されます。
pythonスクリプト
# QGISのプロジェクトファイルがあるディレクトリに画像を保存 image_location = os.path.join(QgsProject.instance().homePath(), "render.png") # 静岡県の指定緊急避難場所データのレイヤー vlayer1 = QgsProject.instance().mapLayersByName("22")[0] # OpenStreetMapのレイヤー vlayer2 = QgsProject.instance().mapLayersByName("OpenStreetMap")[0] options = QgsMapSettings() options.setLayers([vlayer1, vlayer2]) # 後ろのレイヤが下にくる options.setBackgroundColor(QColor(255, 255, 255)) # 背景色設定(今回はOSM読み込むので意味なしだが・・・) options.setOutputSize(QSize(800, 600)) # 画像の出力サイズ options.setExtent(vlayer1.extent()) # これで静岡県のデータがある周辺に設定される options.setDestinationCrs(vlayer1.crs()) # CRSを設定。 # レンダリング用ジョブのオブジェクトを作成 render = QgsMapRendererParallelJob(options) # レンダリングが終わったら画像を保存するようにする def finished(): img = render.renderedImage() img.save(image_location, "png") print("saved") render.finished.connect(finished) render.start()
↓こんな感じですね。
終わるとimage_location
で定めた場所に以下のような画像が保存されます。
注意点としては、レンダリングはPythonスクリプトとは別で処理が走る点ですね。
処理が別で走っているのでstart()
終了後にすぐ保存しようとしてもまだレンダリングが終わっていなくておかしなことになります。
そのため終了後の動作(ここでは画像の保存)をfinish()
関数とrender.finished.connect(finished)
で定義しておいてレンダリングが終わった後に保存するようにしている、ということみたいですね。
今回はすでにプロジェクトに読み込んでスタイルも設定してあるレイヤーを画像化しました。 pyQGISはレイヤー読み込みもスタイル適用もスクリプトでできるので、そこも含めたスクリプトも書けそうです。もっと言えばQGIS起動しなくてもQGISのPython部分だけ起動して画像の生成もできるのでしょうが・・・それはいろいろと設定が面倒くさそう?
Terria (5) GPXをCZMLに変換して読み込んでみる
概要
- GPXファイルをCZMLに変換してTerriaMapに読み込む。
- 変換はpython(特にgpxpy)を使用する
参考
- ルートヒストリー
- Python3.7によるGPXファイルの読み込みと移動距離の算出
- GIS実習オープン教材(CZML入門)
- CZML仕様
GPS記録について
gpxファイルは自分で過去に記録したものを使用しました(さすがにこれだけで個人を特定はできないと思いますが・・・自分のGPSデータを使用するのは気を付けないといけないですね・・・)。探せばネットにもたくさん落ちていると思います。
GPRロガーとしては「ルートヒストリー」を使用しました。シンプルでよいアプリだと思います。
入力に使用したGPXファイル(抜粋)
ルートヒストリーでGPX形式で出力すると以下のようになります。
<?xml version="1.0" encoding="UTF-8"?> <gpx version="1.1" creator="RouteHistory - https://www.ateow.com" xmlns="http://www.topografix.com/GPX/1/1"> <trk> <trkseg> <trkpt lat="35.77923847359902" lon="137.25219315313424"> <time>2022-05-04T06:25:02Z</time> <ele>353.5262222290039</ele> </trkpt> <trkpt lat="35.77924467425494" lon="137.25207535075572"> <time>2022-05-04T06:25:02Z</time> <ele>353.4094467163086</ele> </trkpt> <trkpt lat="35.77933668128351" lon="137.25101184773823"> <time>2022-05-04T06:25:06Z</time> <ele>350.3656341670015</ele> </trkpt> <trkpt lat="35.77934171424567" lon="137.2509021258634"> <time>2022-05-04T06:25:07Z</time> <ele>349.1141156737026</ele> </trkpt> <trkpt lat="35.779352662004264" lon="137.25073872997493"> <time>2022-05-04T06:25:08Z</time> <ele>348.62688992877827</ele> </trkpt> </trkseg> </trk> </gpx>
pythonでGPXをCZMLに変換
jupyter notebookで作業してます。GPXの読み込みはQiitaの記事を参考にpygpxで行い、czmlは「GIS実習オープン教材」を参考に作成。GPXは高さが標高で格納されていますが、czmlでは楕円体高での表現とする必要があるので標高⇒楕円体高の変換が必要です(czmlの仕様にまだ詳しくないので断言はできないですが、おそらく楕円体高しかだめっぽい?)。今回はそこまで精度を求めていないのでざっくり40mを足して変換しています。
czmlの中では時間に依存するポイントと軌跡を表現するためのポリラインの二つを作成しています。
変換に使用したnotebook
import json import os import glob import gpxpy import gpxpy.gpx from pytz import timezone import pandas as pd
def make_df_from_gpx(gpx_path): # https://qiita.com/toran/items/6b4bcd103292164e8e28 # タイムゾーン dt_tz = timezone('Asia/Tokyo') # 日時文字列形式 dt_fmt = '%Y-%m-%dT%H:%M:%SZ' # 配列の初期化 DateTime = [] Lat = [] Lng = [] Alt = [] # GPXファイルの読み込み with open(gpx_path,'r') as gpx_file_r: # GPXファイルのパース gpx = gpxpy.parse(gpx_file_r) # GPXデータの読み込み for track in gpx.tracks: for segment in track.segments: # ポイントデータリストの読み込み points = segment.points # ポイントデータの長さ N = len(points) # ポイントデータの読み込み for i in range(N): # ポイントデータ point = points[i] # データ抽出 datetime = point.time.astimezone(dt_tz).strftime(dt_fmt) lat = point.latitude lng = point.longitude alt = point.elevation # データ代入 DateTime.append(datetime) Lat.append(lat) Lng.append(lng) Alt.append(alt) df = pd.DataFrame(columns=["datetime", "lat", "lon", "alt"]) df["datetime"] = DateTime df["lat"] = Lat df["lon"] = Lng df["alt"] = Alt return df
def make_czml(timestamp, lat, lon, alt, diff_geoid=0): czml = [{ "id" : "document", "name" : "name", "version" : "1.0" }] elem = { "id": "1", "name": "gpx_point_with_timestamp", "description": "GPXの時刻付きポイントです", f"availability":f"{timestamp[0]}/{timestamp[-1]}", "billboard" : { "image" : "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", "scale" : 0.5 }, "position":{ "epoch" : f"{time_stamp[0]}", "cartographicDegrees":[] } } coord = elem["position"]["cartographicDegrees"] for i in range(len(timestamp)): coord += [timestamp[i], float(lon[i]), float(lat[i]), float(alt[i])+diff_geoid] czml.append(elem) elem_2 = { "id" : "2", "name" : "gpx_line", "description" : "GPX全体のポリラインです", "polyline" : { "positions" : { "cartographicDegrees" : [] }, "material" : { "solidColor" : { "color" : { "rgba" : [0, 0, 255, 100] } } }, "width" : 2.5 } } coord_2 = elem_2["polyline"]["positions"]["cartographicDegrees"] for i in range(len(timestamp)): coord_2 += [float(lon[i]), float(lat[i]), float(alt[i])+diff_geoid] czml.append(elem_2) return czml def save_czml(czml, out_dir, out_name): out_path = os.path.join(out_dir, f"{out_name}.czml") with open(out_path, 'w') as f: json.dump(czml, f, indent=4)
gpx_path = "./Log20220504-152502.gpx" out_dir = "./" df = make_df_from_gpx(gpx_path) out_name = os.path.basename(gpx_path).split(".")[0] lat = df["lat"].values lon = df["lon"].values alt = df["alt"].values time_stamp = df["datetime"].values # ざっくり40m程度足して標高⇒楕円体高の変換をする czml = make_czml(time_stamp, lat, lon, alt, diff_geoid=40) save_czml(czml, out_dir, out_name)
Terriaでの表示
Terriamapにドラッグ&ドロップでczmlを読み込むと以下のような感じ。下に出てくるバーで時間を動かして表示できます。 (背景が寂しかったので地理院の空中写真を読み込んでいます。)
- 地理院タイル一覧
- https://maps.gsi.go.jp/development/ichiran.html
- 「簡易空中写真(2004年~)」を読み込んでいます。たまたま画像がある箇所でよかった・・・。