追記(6月25日)スタイル設定スクリプトと説明を修正

要約:
一昨日始めた新しい処理方法のうち、SVGを操作するJavaScriptについて。指定された要素から自動的にビューポートを算出し、指定された塗りや線のスタイルをCSSで設定。CSSを使えないもの(要素の重なり、点の大きさ等)はDOMで何とかしています。下図は一昨日の再掲。
Contents

JavaScriptで行う処理の概要
下は、(1)に載せたHTMLのうちJavaScript部分を抜き出し、説明用に改変したもの。大まかには、処理対象SVGを確定 → 操作用クラスのインスタンスを作成 → ビューポート設定 → スタイル設定、という流れです。
example_view_svg.jsSelectRawtextBitbucket
window.addEventListener('load', function () {
    // ページ読み込み後に処理開始

    // 最初のOBJECTタグのソースがSVGだと想定し、
    // ビューポートとスタイル設定のためのインスタンスを作る
    var obj = document.getElementsByTagName('object')[0],
        svg = obj.contentDocument.documentElement,
        b = new SvgBbox(svg),
        s = new SvgStyle(svg, b);

    // ビューポートのオプション設定
    // b.width = 500;      // 表示横幅. 無指定ならブラウザ横幅
    // b.fringe = 10;      // 周縁ピクセル数. 無指定ならゼロ
    // b.vaspOption = 1.3; // 横に対する縦の補正比率. 無指定なら補正なし

    // カッコ内をIDとする要素を基準に、ビューポート設定
    b.calc('border');

    // 以下、スタイル設定

    // カッコ内をIDとする要素を一番上に持ってくる
    s.setTop('municipality');
    s.setTop('hospitals'); // 後に実行される要素ほど、上に来る
    ..........
    ..........
    
    // カッコ内をIDとする要素に線幅を設定. 単位はピクセル
    s.setStrokeWidth('hospitals', 1);
    ..........
    ..........

    // カッコ内をIDとする点データに半径を設定. 単位はピクセル
    s.setRadius('hospitals', 4);
    s.setRadius('pubs', 40000, true); // 第三引数が真:ピクセルでなく座標値
    
    // 普通のCSSで、塗り、線色、透明度などを自由に設定
    s.css('#border { fill: none; stroke: lightblue }');
    ..........
    ..........
});

ビューポートが決まって初めて座標値の単位とピクセルの比率が決まり、クラスSvgBboxのメンバ変数に入力され、これをスタイル設定クラスのメソッドsetStrokeWidth、setRadiusが使います。従ってこの2メソッドは必ずビューポート設定の後。他のスタイルはビューポート設定前でもいいです。

最後のCSSでは、IDがない要素でもタグ名・クラス名で操作できますが、他は全て要素IDを頼りにします。今後ID以外での操作が必要になったら拡張するかも。あとSVGの仕様は「fill属性がない図形は黒で塗る」なので、塗らない地物は全てfill:noneが必要。


ビューポートの自動設定
クラスSvgBboxを使用。下は(1)に載せたソースの再掲。少し長いのでフレーム内スクロールで表示しています。基本的な使い方はcalcメソッドに要素IDを渡すこと。当該要素をバウンディングボックスとしてSVGビューポートが自動設定されます。必要に応じ、calc実行前にオプションを設定(上のコード中に例あり)。
/* coding: utf-8 */

var SvgBbox = function (svg) {
    this.svg = svg;
    this.width = document.body.clientWidth;
    this.height = null;
    this.fringe = 0;
    this.scale1px = null;
    this.vaspOption = 1;
};

SvgBbox.prototype.calc = function (id) {
    var chd = this.svg.getElementById(id).childNodes,
        aryX = Array(),
        aryY = Array();

    for (var i = 0, len = chd.length; i < len; i++) {
        var ele = chd[i],
            tgn = ele.tagName;

        if (tgn === 'ellipse') {
            aryX.push(Number(ele.getAttribute('cx')));
            aryY.push(Number(ele.getAttribute('cy')));

        } else if (tgn === 'text') {
            aryX.push(Number(ele.getAttribute('x')));
            aryY.push(Number(ele.getAttribute('y')));

        } else if (tgn === 'path') {
            var seg = ele.pathSegList;
            for (var j = 0, len2 = seg.length; j < len2; j++) {
                var s = seg[j];
                if (s.pathSegType === 2 || s.pathSegType === 4) {
                    // only SVGPathSegMovetoAbs or SVGPathSegLinetoAbs
                    aryX.push(s.x);
                    aryY.push(s.y);
                }
            }
        }
    }

    var x1 = Math.min.apply(null, aryX),
        y1 = Math.min.apply(null, aryY),
        x2 = Math.max.apply(null, aryX),
        y2 = Math.max.apply(null, aryY),
        hh = y2 - y1,
        ww = x2 - x1;

    this.scale1px = ww / this.width;
    this.height = hh / this.scale1px;

    var fgx = this.fringe * this.scale1px,
        fgy = this.fringe * this.scale1px / this.vaspOption;
        fg2x = this.fringe * 2,
        fg2y = this.fringe * 2 / this.vaspOption;

    this.svg.setAttribute('preserveAspectRatio', 'none');
    this.svg.setAttribute('height', this.height * this.vaspOption + fg2y);
    this.svg.setAttribute('width', this.width + fg2x);
    this.svg.setAttribute('viewBox', Array(
        x1 - fgx, y1 - fgy, x2 - x1 + fgx * 2, y2 - y1 + fgy * 2).join(' '));
};

calcに渡すのは、ellipse(楕円)、text、pathいずれかを子要素に含むg要素。他の要素は、当面PostGISで出力するSVGに使わなそうなので省略。また
昨日も書きましたが、path要素のd属性は絶対座標のみ対応。より正確には、メソッドMLに続く絶対座標だけを見てバウンディングボックスを決めます。

バウンディングボックスの決め方は単純で、全ての子要素が持つ座標(ellipsetextは中心点、pathは始点と全ての経過点)をX・Y別に配列に入れ、X・Yの最小値・最大値を算出するだけ。今回の例で使ったborderテーブルには約56,500個の座標があり、バウンディングボックス算出に約150200ミリ秒かかりました。より効率的にするなら、calcメソッドに渡す四隅座標だけのg要素をPostGISで作って出力SVGに含めるといいです。

path要素にはpathSegListという点座標の配列があり、各要素のx・y属性で座標値をゲット。自前でpath要素のd属性をパースするのは大変だな~と思っていたところ、下記にpath要素の操作例があって大助かり。

»Stack Overflow : Change svg path with javascript

オプション設定がなければ、バウンディングボックスの四隅がそのままSVGの表示領域となるようにviewBox属性を定め、SVGの横幅はブラウザ内部の横幅いっぱい。オプションのメンバ変数でwidth(横幅)、fringe(周縁の大きさ)、vaspOption(縦横比)が設定されていたら、それに合わせて算出。最終的に確定した「1ピクセル当たりの横方向の座標単位」がメンバ変数scale1pxになり、これをスタイル設定での線幅や点の大きさに使います。

周縁の大きさfringeはピクセルで指定し、この分だけSVGの最終的な表示サイズが四方に拡大。だから例えば「周縁10ピクセル、SVG横幅500ピクセル」にしたければ、width480に設定します。

塗り・線などのCSS設定
クラスSvgStyleを使用。下は(1)に載せたソースの再掲。上のBboxと同様、フレーム内スクロールで表示しています。CSS設定に関する部分は、最後にあるメソッドcss。(翌日追記:メソッドのうちsetStrokeWidthを修正しました。詳細は次項を参照)
geomSvgStyle.jsSelectRawtextBitbucket
/* coding: utf-8 */

var SvgStyle = function (svg, bbox) {
    this.svg = svg;
    this.bb = bbox;
};

SvgStyle.prototype.setTop = function (id) {
    var g = this.svg.getElementById(id),
        p = g.parentNode;
    p.removeChild(g);
    p.appendChild(g);
};

SvgStyle.prototype.setStrokeWidth = function (id, num) {
    var g = this.svg.getElementById(id),
        chd = g.childNodes,
        len = chd.length;
    for (var i = 0; i < len; i++) {
        chd[i].setAttribute('vector-effect', 'non-scaling-stroke');
    }
    g.setAttribute('stroke-width', num);
};

SvgStyle.prototype.setRadius = function (id, num, originalScale) {
    var els = this.svg.getElementById(id).getElementsByTagName('ellipse'),
        len = els.length,
        rx = num,
        ry = num / this.bb.vaspOption;
    if (! originalScale) {
        rx *= this.bb.scale1px;
        ry *= this.bb.scale1px;
    }
    for (var i = 0; i < len; i++) {
        var e = els[i];
        e.setAttribute('rx', rx);
        e.setAttribute('ry', ry);
    }
};

SvgStyle.prototype.css = function (text) {
    var ss = this.svg.parentNode.styleSheets;
    if (ss.length === 0) {
        this.svg.appendChild(document.createElementNS(
            'http://www.w3.org/2000/svg', 'style'));
    }
    ss[0].insertRule(text, 0);
};

処理内容はHTMLCSSのルールを追加する時と同じで、STYLE要素にinsertRuleするだけ。insertRuleを使えないブラウザには未対応で、IEならaddRuleに変更すればいいかもしれませんが、特に必要ないので試してません…。今回、SVGOBJECT要素のソースなのでCSSを追加できるか不明でしたが、やってみたら出来ました。

最近のブラウザではCSS3opacity(透明度)が使えるので、今回のSVGでも設定可能。↓ こんな風に特定の地物の塗りを半透明にすることが、CSSの文法でできます。

s.css('#pubs { fill: green; opacity: 0.5 }');


要素の重なり、線の太さ、点の大きさの設定
使うクラスは上と同じSvgStyle。使うメソッドと引数は次のとおりです。

• 要素の重なり …setTop(要素ID)

• 線の太さ …setStrokeWidth(要素ID, ピクセル数)
• 点の大きさ …setRadius(要素ID, 半径, ピクセルでなく座標値で設定)

要素の重なりは、CSSz-IndexSVGに使えないので変則的な方法。指定された要素のDOMノードをいったん削除し、親要素に再びappend。SVGの仕様で「後ろに記述された要素ほど上に描画される」ので、実質的に親要素の中で一番上に来ます。

線の太さは、
指定されたピクセル数をビューポートの座標単位に変換してDOMでセット。ついでに、縦横比が1:1でない時に備えてvector-effect:non-scaling-strokeも付加。これがないと、縦横比を変えた場合に線の向きによって太さが異なってきます。まぁ細い線だけなら気にする必要ない話ですが。(翌日、以下に修正しました)

まず子要素を全て走査しvector-effect:non-scaling-strokeをセット。これで線幅を、座標単位に関わらずピクセル数で指定できます。また縦横比を変えても、線の向きによらず同じ太さに揃うメリットもあり。わざわざ子要素に処理するのは、g要素を渡した場合、vector-effectが子要素に効果を与えないため(下記参照)。その後、渡された要素にstroke-width属性を設定します。


»svgを使って枠線の幅を指定可能なテンプレートを作る

(修正ここまで)

点の大きさは、ellipse要素を子要素に含むg要素をメソッドに渡す想定。第2引数に数値を入力。第3引数がないかfalseならピクセル数、第3引数がtrueなら座標単位の値に変換してellipse要素のrx・ry属性が設定されます。座標単位がメートルで ↓ のように指定すると半径40kmのバッファ。先ほどの半透明の例がこれで、SVGに事後的にバッファを追加できます。
s.setRadius('pubs', 40000, true);


課題
今回は5つのテーブルをそのままg要素にしてIDを振っただけの簡単なSVGでした。もう少し複雑に、特定の地物にIDやクラスを付けて別のスタイルにするとか、g要素が入れ子になっている場合などで試すと、要素指定の仕方で改善すべき点が出てきそう。

ビューポート設定メソッドは、当初、要素ID指定がない場合のデフォルトの動作を検討していました。先頭のg要素でバウンディングボックスを出すとか、全要素で出すとか。この必要性は、実際使ってみないと分かりません。

今後はSVG中のテキストもきちんと考える予定。PostGISには地物ラベル(注記)の型がないですが、SVG出力時にtext要素として含めれば代用できるかも。あとSVGで設定可能なスタイルは、塗りや線の色・透明度の他に「線種」もあり、今後検証する予定。

明日はブラウザ上でのSVG操作、とくにズームとPDF出力について書きます。