Oracleに接続可能ななるべく小さいpython実行環境のDocker imageをビルドする
今回のお題
接続先がOracleなシステムでPythonが実行可能なコンテナイメージを作りたい。
小さいイメージは正義。
tl;dr
- Oracle公式のイメージを利用する。
- Docker multi stage buildを活用する。
- alpineは苦行。
完成形は最後のほうにあります。
要件
MUST
- 接続先は既存システムのOracle。変更不可。
- 実行環境はパブリッククラウド内。
- コードはgit管理。
- 開発言語は任意。(今回はData Classなどの機能を使ってみたかったのでpython3.7とした)
WANT
- 環境構築は手軽にやりたい。
- デプロイは高速にしたい。
構築までの道のり
まずは素朴に
ローカル確認
Dockernizeする前に、ローカルでの動作を確認します。
pythonからOracleに接続するための公式のライブラリは cx_Oracleです。 *1*2
pipenvで入れましょう。*3
$ pipenv install $ pipenv install cx_Oracle $ cat Pipfile [[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] cx-oracle = "*" [requires] python_version = "3.7"
Oracleインスタンスの構築
接続先のOracleインスタンスもDockerで立てておきます。*4
macOSでOracle Database使いたい - Qiita
上記に従い本体のバイナリを入手します。バージョンは 19.3.0 Standard Edition 2 としました。 *5
$ git clone git@github.com:oracle/docker-images.git $ cd docker-images/OracleDatabase/SingleInstance/dockerfiles $ mv /your/download/path/to/LINUX.X64_193000_db_home.zip 19.3.0/ $ ./buildDockerImage.sh -i -s -v 19.3.0 # 完了まで10分ぐらいかかる
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE oracle/database 19.3.0-se2 91da406c90ef 23 seconds ago 6.65GB oraclelinux 7-slim 874477adb545 6 weeks ago 118MB
無事イメージが作成されました。
とくにカスタマイズはせず起動します。
$ mkdir -p $HOME/tmp/oradata $ docker run --name docker_oracle \ -p 1521:1521 -p 5500:5500 \ -v $HOME/tmp/oradata:/opt/oracle/oradata \ oracle/database:19.3.0-se2 # ..snip.. ######################### DATABASE IS READY TO USE! #########################
起動しました。コンテナが立ち上がっていることが確認できます。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6c7d9db8a2bb oracle/database:19.3.0-se2 "/bin/sh -c 'exec $O…" 16 minutes ago Up 16 minutes (healthy) 0.0.0.0:1521->1521/tcp, 0.0.0.0:5500->5500/tcp docker_oracle
せっかくなのでコンテナ内からOracleに接続してみましょう。
$ docker exec -it docker_oracle /bin/sh $ sqlplus /nolog $ SQL> connect sys/cO11MX9C5Bk=1@ORCLCDB as sysdba Connected. SQL> select SYSDATE from DUAL; SYSDATE --------- 26-SEP-19 SQL>
実行
さて先程構築したOracleに接続してSYSDATEをSELECTする単純なpythonを書きます。
#!/usr/bin/env python3 import cx_Oracle print(cx_Oracle.clientversion()) # パスワードは勝手に払い出される conn = cx_Oracle.connect(user='sys', password='cO11MX9C5Bk=1', dsn='localhost:1521/ORCLCDB', mode=cx_Oracle.SYSDBA) curs = conn.cursor() curs.execute('select SYSDATE from DUAL') row = curs.fetchone() print(f'Hello Oracle ! {row[0]}')
このまま実行するとクライアントライブラリがないエラーがでます。
$ pipenv run ./main.py Traceback (most recent call last): File "./main.py", line 5, in <module> print(cx_Oracle.clientversion()) cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "dlopen(libclntsh.dylib, 1): image not found". See https://oracle.github.io/odpi/doc/installation.html#macos for help
Oracleの公式*7からライブラリをダウンロードして任意の場所に展開します。
今回は $HOME/opt/oracle/instantclient_19_3
としました。 LD_LIBRARY_PATH
も通しておきましょう。
$ pipenv run ./main.py (19, 3, 0, 0, 0) Hello Oracle ! 2019-09-26 02:16:43
無事動きました。ここまで前フリでした。
Dockerfileを書く
ようやくDockerに手を付けます。
上記のpython環境をDockerfileで記述します。
まずはpython3.7およびpipenvの入ったイメージを作成します。
ベースイメージは python:3.7-slim-buster
としました。*8*9
0版
## # Building runtime image. ## FROM python:3.7-slim-buster WORKDIR /usr/local/app ADD Pipfile . ADD Pipfile.lock . ADD main.py . RUN apt-get update -y \ && apt-get -y install \ && pip install --no-cache-dir pipenv \ && pipenv install --deploy ENTRYPOINT ["pipenv", "run"]
$ docker build . -t cx_oracle-slim # 出力は省略 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE cx_oracle-slim latest e7b3dc4bafad About a minute ago 239MB
この時点でのイメージサイズは 239MB です。
課題と対応
さてこの状態ではinstantclientが入っていないため、当然実行時にエラーとなります。
docker run -it cx_oracle-slim ./main.py Traceback (most recent call last): File "./main.py", line 5, in <module> print(cx_Oracle.clientversion()) cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "libclntsh.so: cannot open shared object file: No such file or directory". See https://oracle.github.io/odpi/doc/installation.html#linux for help
ではDockerfileでinstantclientを取得できるようにしたいところですが、Oracleのリソース取得にはライセンスの同意が必要です。
つまり、wgetやcurlでの取得が面倒です。
対処としては以下のような方策が考えられます。
instantclientのzipをgitにコミットしておきイメージビルド時に展開する。一番手軽ではありますが、gitに大きなバイナリ*10をコミットするのは望ましくありません。
instantclientのzipをコードとは異なるgitリポジトリにコミットしておく。コードとは異なるリポジトリにzipのみをコミットしておき、ビルド時にcloneしておく方策です。 コードのリポジトリはキレイになりますが、どこにリポジトリを作るのか、バージョンアップはどうするのか、という将来の課題を残します。*11
Oracle公式イメージをベースにしてpython環境を構築する。Oracle公式がgithubで公開しているイメージ
oraclelinux:x-slim
*12を利用すると、ライセンスの同意などを回避できる模様。素のイメージは368MBです。
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE oracle/instantclient 19 bdf41080d895 4 seconds ago 368MB
1版
運用保守を鑑みて、1,2は捨て、3の方針としました。
このときのDockerfileは以下です。
FROM oraclelinux:7-slim ARG release=19 ARG update=3 RUN yum -y install oracle-release-el7 \ && yum-config-manager --enable ol7_oracle_instantclient \ && yum -y install \ oracle-instantclient${release}.${update}-basic \ oracle-instantclient${release}.${update}-devel \ && rm -rf /var/cache/yum WORKDIR /usr/local/app ADD Pipfile . ADD Pipfile.lock . ENV LC_ALL=en_US.UTF-8 RUN yum update -y \ && yum -y install \ libaio \ python36 \ && rm -rf /var/cache/yum/* \ && yum clean all \ && pip3 install --no-cache-dir pipenv \ && pipenv install --deploy ADD main.py . ENTRYPOINT ["pipenv", "run"]
Dockerで立てたOracleサーバと通信するためにdocker run時にlinkしてやる必要があります。
main.pyの接続先名を localhost
から ora_server
に置き換えたのち実行します。
$ docker run -it --link docker_oracle:ora_server cx_oracle-slim ./main.py (19, 3, 0, 0, 0) Hello Oracle ! 2019-09-26 04:51:55
無事当初の目的である、Docker内からOracleと通信が可能になりました。
課題
しかしながら、oraclelinux自体がCentOSをベースにしているようで、yumでは2019-09-26現在python37は入りません。
python36を妥協して使うにしても出来上がったイメージのサイズは 685MB となかなかなサイズとなりました。
pythonのライブラリが増えてきた時を考えるともう少しダイエットさせたい気持ちがあります。
軽量化の取り組み
さてようやく本題です。
満たしたい事項を箇条書きにします。
- python37を使いたい
- instantclientのバイナリを自前で管理したくない
- イメージサイズは小さくしたい
上記を解決するために、Dockerのmulti-stage buildsを利用します。
疲れてきたのでいきなり最終的なDockerを示します。
最終版
## # Buidling intermediate image. # instantclientのみほしいが、wgetなどではユーザ認証が必要になるため、公式のイメージから拝借する ## FROM oraclelinux:7-slim AS oracle-client ARG release=19 ARG update=3 RUN yum -y install \ oracle-release-el7 \ && yum-config-manager --enable ol7_oracle_instantclient \ && yum -y install \ oracle-instantclient${release}.${update}-basic \ oracle-instantclient${release}.${update}-devel \ && rm -rf /var/cache/yum ## # Building runtime image. ## FROM python:3.7-slim-buster COPY --from=oracle-client /usr/lib/oracle /usr/lib/oracle ## XXX: バージョン指定がバラけてる ARG release=19 ARG update=3 ENV LD_LIBRARY_PATH=/usr/lib/oracle/${release}.${update}/client64/lib WORKDIR /usr/local/app ADD Pipfile . ADD Pipfile.lock . RUN apt-get update -y \ && apt-get -y install \ --no-install-recommends \ libaio1 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir pipenv\ && pipenv install --system --deploy \ && pip uninstall -y \ pipenv \ virtualenv-clone \ virtualenv ADD main.py . ENTRYPOINT ["./main.py"]
最終版のイメージサイズは426MBと、第1版の40%減となりました。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cx_oracle-slim latest bd5a907578db 6 seconds ago 426MB
トピック
以下最終版に至るまでのトピックです。
まず oraclelinux:7-slim
から実行に必要な必要な資材を特定しました。
oracle-instantclient${release}.${update}-xxx
は /usr/lib/oracle
配下にyumでインストールされるようでした。
oraclelinux:7-slim
を AS oracle-client
としておき、こちらを COPY --from=/usr/lib/oracle
でpython37のベースイメージに持ってきます。
この時点でイメージサイズは459MBと、第1版よりも200MB以上小さくなりました。また、python3.7が使える環境です。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cx_oracle-slim latest 9377e5551843 6 seconds ago 459MB
/usr/lib/oracle
配下のファイルは全体で225MB、cx_Oracleにはほぼ必須のもので、削れるにしてもojdbc8.jarなどの4MBでしたのでDockerfileの可読性を踏まえてこれ以上は追求しないこととしました。
次にapt, pipenv周りを削ります。
apt-getでは --no-install-recommends
を付与することで余計なパッケージのインストールを抑止できますが、今回は効果がありませんでした。*13
pipenvは仮想環境を構築しますが、今回はDocker内のためシステムにインストールされるPythonは単一のバージョンです。したがって仮想環境用のバイナリをコピーする必要はありません。
--system
を付与することで、pipenvに書かれたパッケージをsystemにインストールしてくれます。 さらに、pipenvはinstall実行後に不要となりますので、pipからも削除可能です。これで更に30MB削減です。*14
以上です。
*1:https://oracle.github.io/python-cx_Oracle/
*2:プロダクションではSQLAlchemyを経由するのが一般的だと思うが今回は使わない。
*3:pipenvは各自入れておいてください。
*4:Oracle Cloudで楽に済ませようとしたらユーザ登録でカード決済がなぜか通らなくて泣いた。
*5:https://www.oracle.com/database/technologies/oracle-database-software-downloads.html
*6:小一時間かかった…。
*7:Instant Client for macOS (Intel x86)
*8:The best Docker base image for your Python application (July 2019)
*9:当初 `python:3.7-alpine` で試みましたがglibc周りで断念。
*10:zipは75MB程度ある
*11:経験として誰も管理せず腐る傾向にある。
*12:https://github.com/oracle/docker-images/blob/master/OracleInstantClient/dockerfiles/19/Dockerfile
*13:今後必要なパッケージが増える際に効いてくるはずです
夫が転職して1年が経ちました
新卒で入社したSIerから今の会社に転職して2016-04-01付けでちょうど1年になる。
そもそもなんで転職したくなったのか、転職していいことあったのか、
期待した結果が得られたのか、雑にまとめる。*1
転職理由
- 受託開発における、作ったら作りっぱなしの商習慣に不満があった
- 常駐先から強制される旧態依然とした開発環境でフラストレーションがあった
- ユーザに喜ばれているという実感が得たかった
転職して得たかったもの
- 自分の作ったもの/価値を提供しているものを継続的に良くしていっているという実感
- 自分の学びを業務にフィードバックする知的欲求の充足
- 世の中の誰かの課題やニーズを解決できているという満足感
実際どうだったか
だいたいOK
詳しく
- すでに構築されているそこそこ複雑なシステムの中で日々良くしていくというサイクルに組み込まれるのは楽。
- 既存システムの制約を超えない"プログラミング"の範囲では色々試せるし。
- ただし、一旦運用に乗っているものを変えることはやはり、調整コスト、そこまでしていいことあるの?、といった説明責任が生じる。
- 部署がたくさんあって、フロントエンドとバックエンド間の意識の断絶を感じることが最近多い。
- これは自分が信頼や既存システムの知識を獲得していくことでうまく立ち回るしかないのかと思う。
- 自分自身の粗忽さの影響で迷惑かけることがまだまだあるのでなんとかしたい
- 尊敬できる人は多いので、勝手を言わせてもらえば辞めないで欲しい
もっとちゃんと書きたかったけど酔っ払ってるので雑に終わる。
*1:退職エントリはカッコ悪いという風潮はどこから来るのだろうね?
TemplateインターフェースクラスでStrategyパターンを実現する
実装メモ
複数の条件をパスしたレコードだけ抽出するみたいな用途を想定。
#include <iostream> #include <vector> #include <string> /** * Template Interface class */ template <typename T> class Filter{ public: virtual bool filter(const T &v) const = 0; virtual ~Filter(){} }; /** * Templateクラスの継承は親クラスに<>をつける */ class LengthFilter : public Filter<std::string> { public: virtual bool filter(const std::string &v) const { std::cout << "LengthFilter:4" << std::endl; return v.length() > 4; } }; /** * コンストラクタ、メンバ変数をもてるのは通常のclassと変わらない */ class StringFilter : public Filter<std::string>{ const std::string matcher; public: StringFilter(const std::string &s) : matcher(s){} virtual bool filter(const std::string &v) const { std::cout << "StringFilter:" << matcher << ", target:" << v << std::endl; return v == matcher; } }; /** * 異なる型を具体化した継承 */ class IntegerFilter : public Filter<int> { public: virtual bool filter(const int &i) const { std::cout << "IntegerFilter:4" << std::endl; return i > 4; } }; int main() { // std::stringで具体化した子クラスを格納するコンテナ std::vector<std::shared_ptr<Filter<std::string> > > filters; // 型パラメータが同一なら同じコンテナに入れられる filters.emplace_back(new LengthFilter()); filters.emplace_back(new StringFilter("Hoge")); filters.emplace_back(new StringFilter("Hello")); // 型パラメータが異なればコンパイルエラー //filters.emplace_back(new IntegerFilter()); std::vector<std::string> v; v.push_back("Hello"); v.push_back("Hoge"); for(const auto &s : v){ std::cout << "target :"<< s << std::endl; for(const auto &f : filters) { // 異なるFilterクラスをそれぞれ適用する処理 if (f->filter(s)) { std::cout << "*** Match ***" << std::endl; } else { std::cout << "*** Unmatch ***" << std::endl; } } std::cout << std::endl; } return 0; }
ICUでShift-JIS, EUC-JP, UTF-8の相互変換
コード
ヘッダ
#ifndef string_encoder_hpp #define string_encoder_hpp #include <string> namespace encoding { class Encoder{ public: // From EUC-JP static std::string EucToSjis(const std::string &value); static std::string EucToUtf8(const std::string &value); // From Shift-JIS static std::string SjisToEuc(const std::string &value); static std::string SjisToUtf8(const std::string &value); // From UTF-8 static std::string Utf8ToEuc(const std::string &value); static std::string Utf8ToSjis(const std::string &value); }; } #endif /* string_encoder_hpp */
実装
#include <vector> #include <unicode/unistr.h> #include "string_encoder.hpp" namespace encoding { namespace internal { namespace encode_name { //! EUC-JP static const std::string kEucJp = "euc-jp"; //! Shift-JIS static const std::string kShiftJis = "shift-jis"; //! UTF8 static const std::string kUtf8 = "utf8"; } std::string encode(const std::string &value, const std::string &from, const std::string &to){ icu::UnicodeString src(value.c_str(), from.c_str()); // 出力バッファにnullptrを渡し変換後のバイト数が取得する const int length = src.extract(0, src.length(), nullptr, to.c_str()); // 変換 std::vector<char> result(length + 1); src.extract(0, src.length(), &result[0], to.c_str()); return std::move(std::string(result.begin(), result.end() - 1)); } } //! From EUC-JP To sjis std::string Encoder::EucToSjis(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kEucJp, internal::encode_name::kShiftJis)); } //! From EUC-JP To UTF-8 std::string Encoder::EucToUtf8(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kEucJp, internal::encode_name::kUtf8)); } //! From sjis To EUC-JP std::string Encoder::SjisToEuc(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kShiftJis, internal::encode_name::kEucJp)); } //! From sjis To UTF-8 std::string Encoder::SjisToUtf8(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kShiftJis, internal::encode_name::kUtf8)); } //! From UTF-8 To EUC-JP std::string Encoder::Utf8ToEuc(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kUtf8, internal::encode_name::kEucJp)); } //! From UTF-8 To sjis std::string Encoder::Utf8ToSjis(const std::string &value){ return std::move(internal::encode(value, internal::encode_name::kUtf8, internal::encode_name::kShiftJis)); } }
動作確認
#include <iostream> #include <string> #include <vector> #include "string_encoder.hpp" template <typename T> std::string test(const T &expected, const T &actual){ return (expected == actual ? "Match" : "Unmatch!"); } int main(int argc, const char * argv[]) { const std::string utf8_string = "aこれはウにこーど"; std::cout << utf8_string << " is utf8 string, length:" << utf8_string.length() << std::endl; std::cout << std::endl; // utf8 -> sjis const std::string sjis_string = encoding::Encoder::Utf8ToSjis(utf8_string); // binary of "aこれはウにこーど" by sjis const std::string sjis_dump = { 'a', '\x82', '\xb1', // "こ" '\x82', '\xea', // "れ" '\x82', '\xcd', // "は" '\xb3', // "ウ" '\x82', '\xc9', // "に" '\x82', '\xb1', // "こ" '\x81', '\x5b', // "ー" '\x82', '\xc7' // "ど" }; std::cout << "test sjis is " << test(sjis_dump, sjis_string) << std::endl; // sjis -> eucjp const std::string euc_string = encoding::Encoder::SjisToEuc(sjis_string); // binary of "aこれはウにこーど" by eucjp const std::string euc_dump = { 'a', '\xa4', '\xb3', // "こ" '\xa4', '\xec', // "れ" '\xa4', '\xcf', // "は" '\x8e', '\xb3', // "ウ" '\xa4', '\xcb', // "に" '\xa4', '\xb3', // "こ" '\xa1', '\xbc', // "ー" '\xa4', '\xc9' // "ど" }; std::cout << "test euc is " << test(euc_dump, euc_string) << std::endl; // eucjp -> utf8 const std::string return_utf8 = encoding::Encoder::EucToUtf8(euc_string); // check std::cout << "test utf8 is " << test(utf8_string, return_utf8) << std::endl; // 機種依存文字 const std::string euc_contains_machine_dependent_char_string = { 'a', '\xa4', '\xb3', // "こ" '\xa4', '\xec', // "れ" '\xa4', '\xcf', // "は" '\xad', '\xc0', '\x0a', // "㍉" '\xa4', '\xcb', // "に" '\xa4', '\xb3', // "こ" '\xa1', '\xbc', // "ー" '\xa4', '\xc9' // "ど" }; const std::string ㍉ = encoding::Encoder::EucToUtf8(euc_contains_machine_dependent_char_string); std::cout << "test ㍉ is " << test(euc_contains_machine_dependent_char_string, encoding::Encoder::Utf8ToEuc(㍉)) << std::endl; // 絵文字はsjisに含まれていないので変換できない const std::string 🍣 = { '\x3f', '\x0a' }; const std::string sushi_string = "🍣"; std::cout << "test 🍣 is " << test(sushi_string, encoding::Encoder::SjisToUtf8(🍣)) << std::endl; }
結果
aこれはウにこーど is utf8 string, length:25 test sjis is Match test euc is Match test utf8 is Match test ㍉ is Match test 🍣 is Unmatch!
参考文献
自作クラスのコンテナをstd::copyでバイナリとしてファイル出力する
#include <iostream> #include <sstream> #include <fstream> #include <vector> #include <numeric> class MyClass { private: class BitField { public: unsigned int a_: 10; unsigned int b_: 8; unsigned int c_: 12; unsigned int d_: 2; BitField() : a_(0), b_(0), c_(0), d_(0){} BitField(int a,int b, int c, int d) : a_(a), b_(b), c_(c), d_(d){}; std::string to_string() const { std::stringstream ss; ss << a_ << "," << b_ << "," << c_ << "," << d_; return ss.str(); } } bf_; std::string str_; public: MyClass() : str_(""){}; MyClass(int a,int b, int c, int d, const std::string &str) : bf_(a,b,c,d), str_(str){}; std::string to_string() const { std::stringstream ss; ss << bf_.to_string() << "," << str_; return ss.str(); } uint32_t get_dumpsize() const { uint32_t sum = 0; sum += sizeof(bf_); sum += str_.length(); return sum; } friend std::ostream &operator<<(std::ostream &os, const MyClass &my); }; // std::copyを使用するにはoperator<<をオーバロードする std::ostream &operator<<(std::ostream &os, const MyClass &my){ os.put(my.bf_.a_); os.put(my.bf_.b_); os.put(my.bf_.c_); os.put(my.bf_.d_); os.write(my.str_.c_str(), my.str_.length()); return os; } int main(int argc, const char * argv[]) { std::vector<MyClass> my_class_vector; for(int i = 0; i < 10; i++){ my_class_vector.emplace_back(i + 1, i * 100, i * 1000, i + 3, "end!"); } std::ofstream ofs("/Users/usadamasa/Desktop/binaryfile.bin", std::ios::binary); if(!ofs.is_open()){ std::cerr << "File Open Error" << std::endl; return 1; } // stringがメンバ変数にあるため書き込みサイズとクラスの確保しているサイズは異なる std::cout << "sizeof MyClass :" << sizeof(MyClass) << std::endl; std::cout << "write size :" << my_class_vector.at(0).get_dumpsize() << std::endl; for(const auto &v : my_class_vector){ std::cout << v.to_string() << std::endl; } std::copy(my_class_vector.begin(), my_class_vector.end(), std::ostream_iterator<MyClass>(ofs)); }
ファイル出力結果(2進数)
| a | b | c | d| str | +--------+--------+---------------+--+-----------------------------------+ |00000001|00000000|00000000 000000|11|01100101 01101110 01100100 00100001| |00000010|01100100|11101000 000000|00|01100101 01101110 01100100 00100001| |00000011|11001000|11010000 000000|01|01100101 01101110 01100100 00100001| |00000100|00101100|10111000 000000|10|01100101 01101110 01100100 00100001| |00000101|10010000|10100000 000000|11|01100101 01101110 01100100 00100001| |00000110|11110100|10001000 000000|00|01100101 01101110 01100100 00100001| |00000111|01011000|01110000 000000|01|01100101 01101110 01100100 00100001| |00001000|10111100|01011000 000000|10|01100101 01101110 01100100 00100001| |00001001|00100000|01000000 000000|11|01100101 01101110 01100100 00100001| |00001010|10000100|00101000 000000|00|01100101 01101110 01100100 00100001|
Boost::Geometryでmultilinestringをclipping
図形の上に引かれた線を、3*3などで矩形分割する必要があったため調査メモ。
環境
サンプルコード
#include <iostream> #include <vector> #include <boost/geometry/geometry.hpp> #include <boost/geometry/geometries/linestring.hpp> #include <boost/geometry/geometries/multi_linestring.hpp> #include <boost/geometry/geometries/point_xy.hpp> int main(int argc, const char * argv[]) { namespace bg = boost::geometry; using point_d2 = bg::model::d2::point_xy<double>; using linestring_d2 = bg::model::linestring<point_d2>; linestring_d2 ls; ls.push_back(bg::make<point_d2>(1.1,1.1)); point_d2 lp; bg::assign_values(lp, 2.5, 2.1); bg::append(ls, lp); // print 2 points "((1.1, 1.1), (2.5, 2.1))" std::cout << bg::dsv(ls) << std::endl; // sqrt{(2.5-1.1)^2 + (2.1-1.1)^2} = 1.72… std::cout << "distance " << bg::distance(ls.at(0), ls.at(1)) << std::endl; // boxは境界点を持ちlinestringとの計算に使える using box_d2 = bg::model::box<point_d2>; box_d2 b; bg::envelope(ls, b); std::cout << bg::dsv(b) << std::endl; // distance equal to length std::cout << "length " << bg::length(ls) << std::endl; std::cout << "number of points 1: " << ls.size() << std::endl; std::cout << "number of points 2: " << boost::size(ls) << std::endl; std::cout << "number of points 3: " << bg::num_points(ls) << std::endl; // 点から線への垂線の長さ point_d2 p(1.9, 1.2); // 0.38 std::cout << "distance of " << bg::dsv(p) << " to line: " << bg::distance(p, ls) << std::endl; // linestringはsegmentと見做して計算することもできる double d = bg::distance(p, bg::model::segment<point_d2>(ls.front(), ls.back())); std::cout << "distance " << d << std::endl; // linestringは平滑化できる linestring_d2 ls_redundancied = { {3,3}, {3.8, 4}, {6,6}, {4,9}, {5,8}, {7,7} }; linestring_d2 ls_simplified; bg::simplify(ls_redundancied, ls_simplified, 0.5); std::cout << "not simplified :" << bg::dsv(ls_redundancied) << std::endl; // removed (3.8, 4) and (5,8) std::cout << "simplified :" << bg::dsv(ls_simplified) << std::endl; // // linestringのclipping // box_d2 cb(point_d2(1.2, 1.2), point_d2(4.5, 2.5)); std::vector<linestring_d2> clipped; ls.push_back(point_d2(1.3,1.3)); // 矩形, input, output bg::intersection(cb, ls, clipped); std::cout << "before clipped : " << bg::wkt(ls) << std::endl; for(const auto &v : clipped) { // 複数の線ができるが、multilinestringとしては解釈してもらえないのでfor文で確認 std::cout << bg::wkt(v) << std::endl; } // // multilinestringのclipping // using multilinestring = boost::geometry::model::multi_linestring<linestring_d2>; multilinestring mls; linestring_d2 ls_1 = { {1,9}, {2.8, 7}, {3,6}, {4,5}, {5,4}, {6,2} }; mls.push_back(ls_1); linestring_d2 ls_2 = { {1,2}, {5.8, 3}, {5.9,4}, {6.0,5}, {6.1,6}, {9,11} }; mls.push_back(ls_2); box_d2 cb_2(point_d2(2,3), point_d2(6, 9)); multilinestring mls_clipped; bg::intersection(cb_2, mls, mls_clipped); std::cout << "before mls(A) : " << bg::wkt(mls) << std::endl; std::cout << "clipping box(B) : " << bg::wkt(cb_2) << std::endl; std::cout << "after mls(C) : " << bg::wkt(mls_clipped) << std::endl; // // multi_pointのclipping // using bg_multipoint = boost::geometry::model::multi_point<point_d2>; bg_multipoint mp; mp.push_back(point_d2(1.4, 1.4)); mp.push_back(point_d2(2.0, 2.0)); mp.push_back(point_d2(2.9, 2.9)); mp.push_back(point_d2(3.0, 3.0)); bg_multipoint clipped_mp; box_d2 b2(point_d2(1.5,1.5), point_d2(3.0, 3.0)); for(const auto &p : mp){ // intersectionではなくintersectsで1点ずつ確認する必要があるぽい if(bg::intersects(b2, p)){ clipped_mp.push_back(p); } } // MULTIPOINT((1.4 1.4),(2 2),(2.9 2.9),(3 3)) std::cout << bg::wkt(mp) << std::endl; // MULTIPOINT((2 2),(2.9 2.9),(3 3)) std::cout << bg::wkt(clipped_mp) << std::endl; }
目で見て確認*1
before mls(A)
MULTILINESTRING((1 9,2.8 7,3 6,4 5,5 4,6 2),(1 2,5.8 3,5.9 4,6 5,6.1 6,9 11 ))
clipping box(B)
POLYGON((2 3,2 9,6 9,6 3,2 3))
after mls(C)
MULTILINESTRING((2 7.88889,2.8 7,3 6,4 5,5 4,5.5 3),(5.8 3,5.9 4,6 5))
線と矩形の接点にちゃんと点がある。
なお、矩形から完全に外れたlinestringは無視されるらしい。
その他
今回はC++かつ既存コードがboostを使用していてためBoost::Geometryを使ったが、他にもいろいろあるぽい。
参考文献
c++でも畳み込みたい!
よくやる処理に、ある配列またはコンテナに入っている要素の総和(マージ?結合?)したいというものがあるので、
簡潔に書ける方法はないかと調べたところ、STLにはstd::accumulate()
があるらしい。
accumulate
(畳み込み)という操作は既にHaskellを学ぶ中で知っていたため概念としてはすぐに理解できたものの、
色々ハマりやすそうなのでテストコードをメモっておく。
#include <iostream> #include <vector> #include <numeric> #include <string> using namespace std; class myClass { public: uint32_t _num; std::string _str; myClass(const uint32_t num, const std::string str){ _num = num; _str = str; } // `+`演算子オーバーロード myClass operator+(const myClass &y) const { return myClass( this->_num + y._num, this->_str + "-" + y._str); } }; // BinaryOperator class myMerge { public: myClass operator()(const myClass &x, const myClass &y) const { return myClass( x._num * y._num, x._str + "," + y._str); } }; int main() { std::vector<string> list = {"hoge", "fuga", "foo", "bar"}; // 初期値をvectorの第一要素とした場合はbegin()+1を開始要素とする string hogefuga = std::accumulate(list.begin() + 1, list.end(), list[0]); std::cout << "hogefuga:" << hogefuga << std::endl; // -> hogefuga:hogefugafoobar // 初期値を定数にしてもよいならbegin()を開始要素にするとわかりやすいかもしれない string hoge = std::accumulate(list.begin(), list.end(), string("")); std::cout << "hoge :" << hogefuga << std::endl; // -> hoge :hogefugafoobar // この書き方をするとコンテナの第一要素が重複してしまうので注意 string hogehoge = std::accumulate(list.begin(), list.end(), list[0]); std::cout << "hogehoge:" << hogehoge << std::endl; // -> hogehoge:hogehogefugafoobar // 要素が1つしかない場合でも動く std::vector<string> lonely = {"alone"}; string sad = accumulate(lonely.begin() + 1, lonely.end(), lonely[0]); std::cout << "home " << sad << std::endl; // -> home alone // コンテナが空になる可能性があるなら2番目の書き方はできない(SEGVが発生する) std::vector<string> empty = {}; std::cout << "からっぽ:" << std::accumulate( empty.begin(), empty.end(), string("")) << std::endl; // -> からっぽ: // std::cout << "からっぽ:" << std::accumulate(empty.begin() + 1, empty.end(), empty[0]) << std::endl; // -> SEGV // 自前クラスに対するaccumulate std::vector<myClass> myList = { {2, "A"}, {3, "B"}, {4, "C"}}; // `+`演算子オーバーロード myClass myAcc = std::accumulate(myList.begin() + 1, myList.end(), myList[0]); std::cout << "myAcc.num = " << myAcc._num << " myAcc.str:" << myAcc._str << std::endl; // -> myAcc.num = 9 myAcc.str:A-B-C // 関数オブジェクト(BinaryOperator)指定 myClass myAcc2 = std::accumulate(myList.begin() + 1, myList.end(), myList[0], myMerge()); std::cout << "myAcc2.num = " << myAcc2._num << " myAcc.str:" << myAcc2._str << std::endl; // -> myAcc2.num = 24 myAcc.str:A,B,C }