2014/09/04
PostGIS
 >  SVG のパスを取り込む(2)
一昨日の続きで、geeko の SVG ファイル ↓ から path 要素を抽出して PostGIS に取り込む作業メモ。昨日、3次ベジェ曲線を PostGIS の線ジオメトリに近似するストアド関数を作ったので早速使うつもりだったが、その前に全パスをきちんと閉じてポリゴン化し、中マドも反映できるか確認した。実行環境は 2014/08/13 の実機を参照(完成したら当然 openSUSE に移す)。

http://en.wikipedia.org/wiki/File:OpenSUSE_official-logo-color.svg


↓ 今日できた所を R でプロット。コードは最後に。まだ直線だけだが、一応全ての path をポリゴン化でき、中マド(目の部分など)も反映した。これで自由に色を塗れる。


以下、作業経過。元データとして、一昨日 SVG ファイルから path 要素を抽出して作ったテーブル ↓ を使う。14個のパスの色と描画コマンドがある。


一昨日、列 d の描画コマンドを解釈して一本ずつの線に分割し、線種(直線とかベジェ曲線とか)と座標に整理するストアド関数を作った。改めて見直すと、path の終端の Z コマンドを処理してなく、また3次ベジェ曲線のうち S コマンドの処理が不十分だったので修正した。前者はパスをきちんと閉じてポリゴンにするのに必須。後者は ↓ に分かりやすい解説があった。

■ Mozilla Developer Network : SVG チュートリアル Paths
https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths

↓ 修正後のストアドの全文。S コマンドの修正前部分も残したので長くなった。また一昨日の時点では2次ベジェ曲線(Q コマンド)を S と一緒に処理していたが、とりあえず今回の SVG には無いので対象外にした。いずれ必要が出たら追加する。
CREATE OR REPLACE FUNCTION "201409"."04_svg_pathcmd_rev"(text)
RETURNS TABLE(sid int, cid int, str text, cmd text, pxy numeric[])
LANGUAGE plpgsql IMMUTABLE AS $BODY$

/* "201409"."02_svg_pathcmd" を修正
Zz コマンドを処理
Ss コマンドで四点返す
Qq コマンドを対象外に。今後必要なら追加 */

DECLARE r record ;
cmd text ;
cmd_prev text ; -- 追加
ar1 numeric[] ;
ar2 numeric[] ;
ar3 numeric[] ; -- 追加 : コマンド S 用
pxy numeric[] ;
mxy numeric[] ; -- 追加
BEGIN
FOR r IN
WITH b AS (
-- Z コマンドを含むように修正
SELECT (regexp_matches($1, '(\w[\d\-\.\,]*)', 'g'))[1] str
), c AS (
SELECT row_number() over() :: int cid, b.str, left(b.str, 1) lab
FROM b
)
SELECT sum(CASE WHEN lab ~* 'M' THEN 1 END)
over(ORDER BY c.cid) :: int sid, *
FROM c ORDER BY 1, c.cid
LOOP
cmd = r.lab ;
ar1 = string_to_array(regexp_replace(right(r.str, -1)
, '(\d)-', '\1,-', 'g'), ',') ;
IF cmd ~* 'M' THEN
pxy = CASE WHEN cmd = 'M' THEN ar1
ELSE ARRAY[pxy[1] + ar1[1], pxy[2] + ar1[2]]
END ;
mxy = pxy ; -- 追加
ar2 = NULL ;
ELSEIF cmd ~* 'Z' THEN -- このブロック追加
ar2 = CASE WHEN pxy = mxy THEN NULL :: numeric[]
ELSE pxy || mxy END ;
ELSEIF cmd ~* '[HVLT]' THEN
ar2 = pxy || CASE
WHEN cmd = 'H' THEN ARRAY[ar1[1], pxy[2]]
WHEN cmd = 'h' THEN ARRAY[pxy[1] + ar1[1], pxy[2]]
WHEN cmd = 'V' THEN ARRAY[pxy[1], ar1[1]]
WHEN cmd = 'v' THEN ARRAY[pxy[1], pxy[2] + ar1[1]]
WHEN cmd ~ '[LT]' THEN ar1
ELSE ARRAY[pxy[1] + ar1[1], pxy[2] + ar1[2]]
END ;
pxy = ar2[3:4] ;
/* 修正前
ELSEIF cmd ~* '[QS]' THEN
ar2 = pxy || CASE
WHEN cmd ~ '[QS]' THEN ar1
ELSE ARRAY[pxy[1] + ar1[1]
, pxy[2] + ar1[2]
, pxy[1] + ar1[3]
, pxy[2] + ar1[4]]
END ;
pxy = ar2[5:6] ; */
-- 以下ブロック修正後
ELSEIF cmd ~* 'S' THEN
IF cmd_prev ~* '[CS]' THEN
-- 起点の制御点が直前ベジェの終点の制御点の対向
ar3 = ARRAY[pxy[1] + (pxy[1] - ar2[5])
, pxy[2] + (pxy[2] - ar2[6])] ;
ar2 = pxy || ar3 || CASE
WHEN cmd = 'S' THEN ar1
ELSE ARRAY[pxy[1] + ar1[1]
, pxy[2] + ar1[2]
, pxy[1] + ar1[3]
, pxy[2] + ar1[4]]
END ;
ELSE
-- 制御点二つが同じ位置
ar2 = pxy || CASE
WHEN cmd = 'S' THEN ar1[1:2] || ar1
ELSE ARRAY[pxy[1] + ar1[1]
, pxy[2] + ar1[2]
, pxy[1] + ar1[1]
, pxy[2] + ar1[2]
, pxy[1] + ar1[3]
, pxy[2] + ar1[4]]
END ;
END IF ;
pxy = ar2[7:8] ;
ELSEIF cmd ~* 'C' THEN
ar2 = pxy || CASE
WHEN cmd = 'C' THEN ar1
ELSE ARRAY[pxy[1] + ar1[1]
, pxy[2] + ar1[2]
, pxy[1] + ar1[3]
, pxy[2] + ar1[4]
, pxy[1] + ar1[5]
, pxy[2] + ar1[6]]
END ;
pxy = ar2[7:8] ;
END IF ;
cmd_prev = cmd ; -- 追加
IF ar2 IS NOT NULL THEN
RETURN QUERY SELECT r.sid, r.cid, r.str, cmd, ar2 ;
END IF ;
END LOOP ;
END ;
$BODY$ ;

-- Ex
WITH a AS (
SELECT *, "201409"."04_svg_pathcmd_rev"(d) rec
FROM "201409"."02_svg_path"
)
SELECT pid, (rec).* FROM a ;

↓ 使用例。今回追加した Z コマンドが処理されている。元テーブルの d 属性をそのままストアドに渡し、各パスを一つずつの線に分割した表を得る。列 cmd が線種、列 pxy が座標。直線は2点(4要素)、3次ベジェは4点(8要素)の配列になる。


ベジェ曲線の処理は次回とし、先に全パスを直線と見なして PostGIS のポリゴンにする。使う主な関数は ↓ の二つ。後者は、中マドのあるポリゴンも簡単に作れて便利。

■ PostGIS 2.1 Manual : ST_LineMerge, ST_MakePolygon
http://postgis.net/docs/manual-2.1/ST_LineMerge.html
http://postgis.net/docs/manual-2.1/ST_MakePolygon.html

間もなくベジェ曲線の処理を追加するのでストアドにはせず、↓ 普通の SQL にした。万一閉じていないパスがあればポリゴンにできずエラーになる。実行結果も合わせて示す。
WITH a AS (
SELECT * FROM "201409"."02_svg_path"
), b AS (
SELECT pid, "201409"."04_svg_pathcmd_rev"(d) rec FROM a
), c AS (
SELECT pid, (rec).* FROM b
), d AS (
SELECT pid, sid, cid, pxy, array_length(pxy, 1) len FROM c
), e AS (
SELECT pid, sid, cid
, ST_MakeLine(ST_Point(pxy[1], pxy[2])
, ST_Point(pxy[len - 1], pxy[len])) geom
FROM d
ORDER BY pid, sid, cid
), f AS (
SELECT pid, sid, ST_LineMerge(ST_Collect(geom)) geom
FROM e
GROUP BY pid, sid
), g1 AS (
-- Exterior
SELECT * FROM f WHERE sid = 1
), g2 AS (
-- Interior
SELECT pid, array_agg(geom) geom FROM f WHERE sid > 1 GROUP BY pid
), g3 AS (
SELECT pid, CASE WHEN g2.pid IS NULL THEN ST_MakePolygon(g1.geom)
ELSE ST_MakePolygon(g1.geom, g2.geom) END geom
FROM g1 LEFT JOIN g2 USING (pid)
)
SELECT pid, fill, ST_AsText(geom)
FROM a JOIN g3 USING (pid)
ORDER BY pid ;


無事、14個の path 要素すべてをポリゴンにできた。これを R で読み込みプロット。↓ クエリの最後に描画色ごとに ST_Collect でまとめた。この方がプロット時の手間が少ない。ブロック e で縦軸を逆転させているのは SVG と PostGIS の縦軸方向が逆なためで、一昨日と同様。
library(RPostgreSQL)
library(maptools)
library(rgeos)
con = dbConnect(PostgreSQL(), dbname='xxx', port=xxx
, user='xxx', password='xxx')
df1 = dbGetQuery(con, '
WITH a AS (
SELECT * FROM "201409"."02_svg_path"
), b AS (
SELECT pid, "201409"."04_svg_pathcmd_rev"(d) rec FROM a
), c AS (
SELECT pid, (rec).* FROM b
), d AS (
SELECT pid, sid, cid, pxy, array_length(pxy, 1) len FROM c
), e AS (
-- Inverse Y
SELECT pid, sid, cid, ST_MakeLine(
ST_Point(pxy[1], pxy[2] * -1)
, ST_Point(pxy[len - 1], pxy[len] * -1)) geom
FROM d
ORDER BY pid, sid, cid
), f AS (
SELECT pid, sid, ST_LineMerge(ST_Collect(geom)) geom
FROM e
GROUP BY pid, sid
), g1 AS (
-- Exterior
SELECT * FROM f WHERE sid = 1
), g2 AS (
-- Interior
SELECT pid, array_agg(geom) geom FROM f WHERE sid > 1
GROUP BY pid
), g3 AS (
SELECT pid, CASE WHEN g2.pid IS NULL THEN
ST_MakePolygon(g1.geom)
ELSE ST_MakePolygon(g1.geom, g2.geom)
END geom
FROM g1 LEFT JOIN g2 USING (pid)
)
-- Group by color
SELECT fill, ST_AsText(ST_Collect(geom))
FROM a JOIN g3 USING (pid)
GROUP BY fill
ORDER BY fill ;
')
invisible(dbDisconnect(con))
windows(height=(340-46)/96, width=(444-8)/96)
par(plt=rep(c(1,99)/100, 2), xaxs='i', yaxs='i')
for(i in 1:nrow(df1)){
plot(add=ifelse(i==1, F, T)
, readWKT(df1[i,2]), border=1, col=df1[i,1])
}


↓ 実行結果(冒頭の再掲)。次はいよいよベジェ曲線の処理に進む。
<< SVG のパスを取り込む(3)
3次ベジェ曲線の近似 >>