ビーガイギーとウェブマップ

In ニュース, マップ, 測定 by Nick Dolezal

[:ja]セーフキャスト・ボランティアからの活動報告には毎回物語があります。

何にせよ、私たちはみなさんの貢献を可視化してSafecast webmap(セーフキャストのウェブ上の地図)に共有するためのツールを追加しています。

ビーガイギーのログはどこが新しいですか?

  1. 1. 複数ログディスプレイ
    1. あなたのコンピューターが処理できる限りのたくさんのログを参照することができます。
  2. 2. 高いパフォーマンス
    1. もちろんパフォーマンスが低ければ複数のログを参照することは問題となることもあるが、幸運なことに高いパフォーマンスとなっている
  3. 3. 動的記号
    1. 色はmapの残り部分と一致します。これによりポイント間の道筋とともに次のマーカーへの方向を見ることができます。
  4. 4. 直URLリンク
    1. Webmapのその他の機能と同様に、bGeigieログには他にリンクできるURLが反映されています。あなたが提出した特定のログを誰かに見せたいですか?それが可能になりました。
  5. 5. シングルビジュアリゼーション
    1. ログをみている時に標準マップレイヤーが可能になりました、そして特定のログと集められたデータセットを比較することができるようになりました。

 

地図上でログを見るには

1. レイヤー選択ボックスでで “bGeigie Log”をクリックしてください。

map_bv1_add_224x304

2. ログコントロールパネルが表示されます。

map_bv2_api_417x148

ログIDがわからない場合は、APIサイトを参照ください。

map_bv3_xfm_207x221

3.  ログIDを参照して”Add”をクリックするとData Transfer Vewが参照できます。背景のMap上でログがアップロードされた場所を見ることができます。memeはこちらです。

map_bv4_markers_310x216

4.  ログを参照できます。マーカー上の線は、進行方向を指しています。これでログを取得した際の道筋を表示しています。

注意: 進行方向線は、モバイル機器では表示されません。

 

上級の使用方法

上級の使用方法こちらのAPIサイトでも参照することができます。

ユーザIDは、下記の通り、最初の文字に”u”をつける必要があります。

map_bv5_userid_184x27

ユーザIDを確認した後、先頭に”u”を追加し、Standard LogIDのテキストボックスに入力してください。

 

技術的な議論

パフォーマンスのために、最も大事なことは、表示するマーカーの最小スケール範囲を設定していただくことです。これはマーカーの全てのそしてそれぞれの拡大表示レベルにおいて、一方のマーカーが非常に大きな値でない限り、それぞれ2ピクセルの範囲内で2つのマーカーを表示することなく、繰り返し行われます。これにより、ユーザビリティーが格段に良くなります。

動的マーカーは、HTML Canvasを利用したクライアントアプリケーション上に変換され、それからPNG データURIに変換されます。最初の実装では、非圧縮GIFに変換され、その方がより早くファイルサイズも小さいです。残念ながらGIFフォーマットでは、パレタイジングなしでアンチエイリアス処理をする簡単な方法はなく、アンチエイリアス処理なしに小さなサークルを作ることができません。幸運にも非常に多くのデータポイントによって描画された比較的少ない数の別々のマーカーがあるので、パフォーマンスの違いはあまり問題になりません。予想外に、Base64エンコードされたPNGファイルをBLOBオブジェクトURLとしてアクセス可能なバイナリイメージにヒープメモリを消費せずに変換することは、マップのレンダリングパフォーマンスを劣化させました。SVG方式も検討しましたが利用できないほど処理が遅かったです。

動的マーカーはどのように指定された幅や高さもサポートしていますが、これに関してユーザコントロールは、初期バージョンでは実装されていません。仕様としては、サイズは、現在、Retina/HDPIディスプレイでは2倍まで対応となっています。しかしながら、現在アクティブなディベロッパー設定は、当分残したままです。URLのクエリーストリングのパラメータでは、例えば、30×30ピクセルのマーカーアイコンに対しては、”iconsize=30”を追記します。”png=0”はPNGの代わりにGIFマーカーを意味します。”svg=1”は、とても動きの遅い動的SVGアイコンとなります。PNGやGIFに対する”blob=1”はオブジェクトURL付きのバイナリBLOBオブジェクトを指します。

注記)上記のような記述が反映のためのページロードのあいだ、URLはクエリストリングとなっています。

マーカーの色は、もともと、iOS/OS XアプリのCプログラミングコードからの直接ポーティングで得られます。インデックスカラーGIFのサポートに対しては、多少の変更が必要で、LUTインデックスの参照が、RGBA8888に関してでさえ必要になってきます。LUT128色に離散化され、モバイル機器では64色に離散化されます。これは、感知できるほどのコントラストの劣化は見られないですし、小数のはっきり異なった画像は、結果としてすごく向上したマップUIの性能を実現しています。これは、URIが値ではなく参照で保存されるという事実です。Google Maps APIは、複製されたマーカー画像に対して何かしら反復的な内部インデクス処理を行っているように見えるし、メリットをいかした、処理だと思えます。

最後に、初期バージョンがこれら全てを可能にしたのではありません。最初の頃のバージョンは、オリジナルのログの道筋の代替ビューを見せながら、マーカーシンボル体系に色づけられたエレベーションプロファイルを表示しており、それは表示されたマーカーに動的にリンクされていました。何度もお蔦するようですが、私たちはデータとそのデータを集めたボランティアの方々のストーリーをベストな方法でお伝えしたいと考えました。 残念ながら、エレベーションプロファイルの裏側のチャート作成は、たくさんのデータポイントに関する問題があり、全てのデータが表示されるかどうかに関していちかばちかの方法でした。

 

ソースコード

フルソースは、webmap、bgeigie_viewer.jsと同じパスにて利用可能です。下記は、マーカーに拡大レベルスケール範囲を設定するためのパフォーマンスに大きく影響するコードになります。

function AssignScaleVisibilityToLines(lines)
{
    var mag, se, lat,lon,cpm, z, zdest, i, j, zdest_n, match;
    var m2dd = 0.00001;                 // approximation for arbitrary latitude.
    var src  = new Float64Array(lines.length * 4);
    var i4   = 0;
    
    // The input, lines, is an array of objects, which is relatively slow to 
    //   iterate through many times.
    // Here, the relevant xyz values are copied to a temporary typed array, 
    //   which improved performance.
    // Float64 was (unfortunately) required to maintain precision for EPSG:4326 
    //   coordinates at higher zoom levels.
    for (i=0; i<lines.length; i++)
    {
        src[i4]   = lines[i][0];
        src[i4+1] = lines[i][1];
        src[i4+2] = lines[i][2];
        src[i4+3] = lines[i][5];
        i4 += 4;
    }//for
    
    zdest   = new Float64Array(lines.length * 3);
    zdest_n = 0;

    for (z=0; z<=21; z++)
    {
        // Convert 2 pixels at zoom level z to a decimal degree approximation
        se  = bv_gbGIS_MetersForLatPxZ_EPSG3857(0.0, 2.0, z) * m2dd; 
        
        // Factor CPM must exceed to bypass spatial filter, up to 10x
        mag = 1.32 + ((21.0 - parseFloat(z)) / 21.0) * 8.68;
        se *= se;                           // Prevent evil sqrt in inner loops
        mag = 1.0 / mag;                    // Prevent evil fdiv in inner loops
                
        for (i=0; i<lines.length*4; i+=4)
        {
            if (src[i+3] == -1.0)
            {
                lat   = src[i];
                lon   = src[i+1];
                cpm   = src[i+2] * mag;
                match = false;
                
                // Only include the point in the results for this zoom level
                // if either 1) the distance (Pythagoreas') isn't near any
                // existing points, or 2) the value is significantly higher.
                for (j=0; j<zdest_n*3; j+=3)
                {
                    if (   (zdest[j]   - lat) 
                         * (zdest[j]   - lat) 
                         + (zdest[j+1] - lon) 
                         * (zdest[j+1] - lon)  < se
                        &&  zdest[j+2]         >= cpm)
                        {
                            match = true;
                            break;
                        }//if
                }//for
            
                if (!match)
                {
                    zdest[zdest_n*3]   = lat;
                    zdest[zdest_n*3+1] = lon;
                    zdest[zdest_n*3+2] = src[i+2];
                    zdest_n++;
                
                    src[i+3] = z;
                }//if
            }//if
        }//for
    }//for
    
    zdest = null;

    // Copy results back to lines (in-place).
    // This value will be set as the property "ext_min_z" on the marker objects.

    i4 = 0;
    for (i=0; i<lines.length; i++)
    {
        lines[i][5] = src[i4+3];
        i4 += 4;
    }//for
    
    src = null;
}//AssignScaleVisibilityToLines


// Based on: http://msdn.microsoft.com/en-us/library/bb259689.aspx
function bv_gbGIS_MetersForLatPxZ_EPSG3857(lat,px,z)
{
    return (Math.cos(lat*Math.PI/180.0)
            *2.0*Math.PI*6378137.0
            /(256.0*Math.pow(2.0,z)))*px;
}//gbGIS_MetersForLatPxZ_EPSG3857

 

翻訳:Shin-ichiro Yamamoto[:en]

bGeigie + Webmap

Every drive by Safecast volunteers tells a story.  Hopefully an exciting and highly radioactive one.  But in any case, we’ve added tools to help you visualize and share these efforts to the Safecast webmap.

What’s new with this bGeigie log viewer?

  1. Multiple Log Display
    1. You can now view as many logs at one time as your computer can handle.
  2. High Performance
    1. Of course, viewing multiple logs wouldn’t be that great unless the performance was abnormally good.  Fortunately, it is.
  3. Dynamic Symbology
    1. Colors that match the rest of the map.  This also allows for showing the direction to the next marker; showing a path within the points.
  4. Direct URL Linking
    1. Like the rest of the options on the webmap, bGeigie logs are reflected in the URL which you can link to others.  Want to show someone a particular set of logs you submitted?  Now you can.
  5. Single Visualization
    1. The standard map layers are available when viewing a log, and allow for comparisons between any particular log and the overall aggregated dataset.

 

Howto: Viewing Logs On the Map

1. Under the layer selection, click “Add bGeigie Log…”

map_bv1_add_224x304

2. The log control panel will then display.

map_bv2_api_417x148

Don’t know the log ID?  Check the API site here.

map_bv3_xfm_207x221

3.  After entering the ID(s) and clicking “Add”, you’ll see the Data Transfer view. The background map shows the location of the logs as they are loaded.  Yes, there’s a meme for that.

map_bv4_markers_310x216

4.  You’re now viewing a log.  The lines on the marker indicate the direction to the next, showing the path taken when acquiring the log.

Note that for performance, the vector bearing line is not rendered on mobile devices.

 

Advanced Usage

You can also load the 25 latest logs for a specific user.  This time, you’ll need a user ID, not a log ID.  You can find those on a different page on the API site here.

User IDs must be prefixed with the character “u”, as depicted below.

map_bv5_userid_184x27

After you look up your user ID, enter it into the standard log ID text box, prefixed with a “u” as shown here.

 

Technical Discussion

For performance, the most important thing done is setting minimum scale ranges for the markers to display at.  This is done by iterating over every zoom level for the markers, and for each zoom level, not displaying two markers within two pixels of each other unless one is a significantly higher value.  This made a very dramatic difference in terms of usability.

Dynamic markers are rendered on the client using HTML5 Canvas, and then to PNG data URIs.  Earlier implementations rendered uncompressed GIFs, which are actually faster and smaller in this case.  Unfortunately for the GIF format, there was no easy way to do antialiasing without palletizing, and you can’t make small circles look good without antialiasing.  Fortunately, there are a relatively small number of distinct markers rendered even for tens of thousands of data points, so the performance difference was a non-issue.  Unexpectedly, converting the base-64 encoded PNGs to binary images accessible as blob/object URLs, while reducing heap usage, decreased the map’s overall rendering performance.  SVG was also explored, but was unusably slow.

While dynamic markers support any arbitrary width and height, user controls for this were not implemented in this initial version; the size is currently only adjusted to render at 2x on retina/HDPI displays.  However, some currently-active developer settings remain for the time being.  In the URL querystring parameters, add “iconsize=30” for example, for 30×30 pixel marker icons.  “png=0” uses GIF markers instead instead of PNG.  “svg=1” enables the very-slow dynamic SVG icons.  “blob=1”, for PNG and GIF, uses binary blobs with an object URL.  Caveat: the URL must already have a driveid in the querystring during page load for any of these have an effect.

The colors for the markers were originally obtained via an almost straight port of the C code from the iOS / OS X app.  This ended up requiring some modifications to support indexed color GIFs better, and references to a single LUT index are kept even with RGBA8888 PNGs.  The LUT is discretized to 128 colors, and on mobile devices, 64 colors.  There was not an appreciable loss of contrast and fewer distinct images resulted in significantly improved map UI performance.  This is despite the fact data URIs are stored by value and not by reference.  It seemed the Google Maps API was doing some kind of internal indexing for duplicate marker images, and it made sense to take advantage of that.

Finally, not everything made it in the initial cut.  Earlier versions had an elevation profile displayed that was also colored with marker symbology, showing an alternate view of the original path of the log, and it was dynamically linked to the markers being displayed.  Again, we wanted to tell the story of the data and the volunteers who collected it in the best way possible.  Unfortunately, the chart generation behind the elevation profile ended up having issues with large numbers of data points, and it was rather hit-or-miss on whether all the data would be displayed.

 

Source Code

The full source is available via the same path as the webmap, as bgeigie_viewer.js.

Included below is the code to assign the zoom level scale ranges to the markers, which was critical for performance.

function AssignScaleVisibilityToLines(lines)
{
    var mag, se, lat,lon,cpm, z, zdest, i, j, zdest_n, match;
    var m2dd = 0.00001;                 // approximation for arbitrary latitude.
    var src  = new Float64Array(lines.length * 4);
    var i4   = 0;
    
    // The input, lines, is an array of objects, which is relatively slow to 
    //   iterate through many times.
    // Here, the relevant xyz values are copied to a temporary typed array, 
    //   which improved performance.
    // Float64 was (unfortunately) required to maintain precision for EPSG:4326 
    //   coordinates at higher zoom levels.
    for (i=0; i<lines.length; i++)
    {
        src[i4]   = lines[i][0];
        src[i4+1] = lines[i][1];
        src[i4+2] = lines[i][2];
        src[i4+3] = lines[i][5];
        i4 += 4;
    }//for
    
    zdest   = new Float64Array(lines.length * 3);
    zdest_n = 0;

    for (z=0; z<=21; z++)
    {
        // Convert 2 pixels at zoom level z to a decimal degree approximation
        se  = bv_gbGIS_MetersForLatPxZ_EPSG3857(0.0, 2.0, z) * m2dd; 
        
        // Factor CPM must exceed to bypass spatial filter, up to 10x
        mag = 1.32 + ((21.0 - parseFloat(z)) / 21.0) * 8.68;
        se *= se;                           // Prevent evil sqrt in inner loops
        mag = 1.0 / mag;                    // Prevent evil fdiv in inner loops
                
        for (i=0; i<lines.length*4; i+=4)
        {
            if (src[i+3] == -1.0)
            {
                lat   = src[i];
                lon   = src[i+1];
                cpm   = src[i+2] * mag;
                match = false;
                
                // Only include the point in the results for this zoom level
                // if either 1) the distance (Pythagoreas') isn't near any
                // existing points, or 2) the value is significantly higher.
                for (j=0; j<zdest_n*3; j+=3)
                {
                    if (   (zdest[j]   - lat) 
                         * (zdest[j]   - lat) 
                         + (zdest[j+1] - lon) 
                         * (zdest[j+1] - lon)  < se
                        &&  zdest[j+2]         >= cpm)
                        {
                            match = true;
                            break;
                        }//if
                }//for
            
                if (!match)
                {
                    zdest[zdest_n*3]   = lat;
                    zdest[zdest_n*3+1] = lon;
                    zdest[zdest_n*3+2] = src[i+2];
                    zdest_n++;
                
                    src[i+3] = z;
                }//if
            }//if
        }//for
    }//for
    
    zdest = null;

    // Copy results back to lines (in-place).
    // This value will be set as the property "ext_min_z" on the marker objects.

    i4 = 0;
    for (i=0; i<lines.length; i++)
    {
        lines[i][5] = src[i4+3];
        i4 += 4;
    }//for
    
    src = null;
}//AssignScaleVisibilityToLines


// Based on: http://msdn.microsoft.com/en-us/library/bb259689.aspx
function bv_gbGIS_MetersForLatPxZ_EPSG3857(lat,px,z)
{
    return (Math.cos(lat*Math.PI/180.0)
            *2.0*Math.PI*6378137.0
            /(256.0*Math.pow(2.0,z)))*px;
}//gbGIS_MetersForLatPxZ_EPSG3857

 [:]