bGeigies on Webmap

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