Sunday, April 6, 2014

WhirlyViz 1.3 - Javascript Update

WhirlyViz is a geospatial visualization engine for mobile devices.  It's based on my WhirlyGlobe-Maply open source library.  With it, you can whip together a pretty data visualization for an iPad or iPhone.



I license the engine to clients, but the WhirlyViz app is free and the best demonstration of the engine itself.

Javascript


WhirlyViz is eminently configurable.  That's kind of the point, but I wasn't entirely satisfied with what we had in 1.2.  Now I know why.  With version 1.3 we've switched to Javascript.

Apple added support for JavaScriptCore in ios7.  They put together a fairly nice bridge between Objective-C and Javascript to go with it.  This gives you a clean, empty Javascript interpreter to use in your app.

That's just what I did.  WhirlyViz configuration files are now Javascript.  Whenever a user taps on something, or a remote vector tile needs to be loaded, or even when the visualization starts up you're running a Javascript function.

This example talks to a WMS server.

// Polluting the global name space with spherical mercator extents
var xMin = -20037508.34;
var yMin = -20037508.34;
var xMax = 20037508.34;
var yMax = 20037508.34;
var xSpan = xMax - xMin;
var ySpan = yMax - yMin;
// Current center of the queries
var centerLon = -75.20896911621094;
var centerLat = 40.024459635387906;
// Where we store parameter data for the tiles
var transitInfo = new Object();
// Called by the image tile loader on a random thread for every tile we need to load
// We just return the URL
var tileurl = function(x,y,level)
{
num = 1 << level;
cellX = xSpan / num;
cellY = ySpan / num;
tileXmin = xMin + x * cellX;
tileXmax = xMin + (x + 1.0) * cellX;
tileYmin = yMin + y * cellY;
tileYmax = yMin + (y + 1.0) * cellY;
// console.log(level + ": (" + x + "," + y + ")" + "size: (" + cellX + "," + cellY + ")");
url = "http://transit.geotrellis.com/api/travelshed/wms?service=WMS&request=GetMap&version=1.1.1&layers=&styles=&format=image/jpeg&transparent=false&height=256&width=256&latitude=" + centerLat + "&longitude=" + centerLon + "&time=" + transitInfo.time + "&duration=3600&modes=" + transitInfo.mode + "&schedule=" + transitInfo.date + "&direction=departing&breaks=600,900,1200,1800,2400,3000,3600,4500,5400,7200&palette=0xF68481,0xFDB383,0xFEE085,0xDCF288,0xB6F2AE,0x98FEE6,0x83D9FD,0x81A8FC,0x8083F7,0x7F81BD&srs=EPSG:3857&bbox=" + tileXmin + "," + tileYmin + "," + tileXmax + "," + tileYmax;
// console.log("Fetching tile: " + url);
return url;
}
// Note: No reason to make these global
var curLon = 0.0;
var curLat = 0.0;
// This is the transportation distance layer
var transLayer = null;
var resetTransLayer = function()
{
// Tear down the old transportion layer
if (transLayer)
transLayer.remove();
// Transportation overly with an active tile URL callback
transLayer = wviz.addImageTileLayer(
{
name: "transit layer",
cache: false,
flipy: true,
coordSys: "EPSG:3857",
minZoom: 10,
maxZoom: 20,
drawPriority: 10,
alpha: 0.75,
tileURLFunc: tileurl
});
}
// Called when the user taps at a location. Called on the main thread so don't block.
wviz.events.onTap = function(lon,lat)
{
}
// Called when the user taps and hold on a location. Called on the main thread.
wviz.events.onPress = function(lon,lat)
{
centerLon = lon;
centerLat = lat;
// Center changes so reload everything
resetTransLayer();
}
// Called when the app view is first initialized
wviz.events.onStartup = function()
{
// Background color
wviz.setBackgroundColor("#FFFFFFFF");
// Name up top
wviz.setTitle("GeoTrellis Transit: Philadelphia");
// Legend on the lower left
wviz.setLegend("<html><body style=\"background-color=black;font-size:18;text-align:center;\"><b style=\"color:#F48380\">0m</b> <b style=\"color:#FAB282\">10m</b> <b style=\"color:#FDDF84\">15m</b> <b style=\"color:#DCF288\">20m</b> <b style=\"color:#B6F2AE\">30m</b> <b style=\"color:#98FEE6\">40m</b> <b style=\"color:#83D9FD\">50m</b></body></html>","#000000AA");
// Background layer with a map
backLayer = wviz.addImageTileLayer(
{
tileJson: "http://a.tiles.mapbox.com/v3/azavea.map-zbompf85.json",
minZoom: 0,
maxZoom: 22,
drawPriority: 0
});
// Bike or walk control
wviz.addControl(
{
name: "transitType",
"display name": "Walk or Bike",
type: "list",
"default": "Bike",
"initial index": 0,
"values":[
"Walk",
"Bike"
]
});
// Regional rail
wviz.addControl(
{
name: "regionalRail",
"display name": "Regional rail",
type: "list",
"default": "No",
"initial index": 0,
"values":[
"No",
"Yes"
]
});
// Bus & Subway
wviz.addControl(
{
name: "busSubway",
"display name": "Bus & Subway",
type: "list",
"default": "No",
"initial index": 0,
"values":[
"No",
"Yes"
]
});
// Day of week or weekend control
wviz.addControl(
{
"name":"Date",
"display name":"Date",
"type":"list",
"default":"Bike",
"initial index":0,
"values":[
"Weekday",
"Saturday",
"Sunday"
]
});
// Time of day control
wviz.addControl(
{
"name": "Time",
"display name": "Departure Time",
"type": "time",
"default": "09:00:00",
"min": "00:00:00",
"max": "23:30:00"
});
// Call the config routine to set defaults
wviz.events.onConfig();
}
// Called when the controls are edited and changed. Called on the main thread
wviz.events.onConfig = function()
{
// console.log("onConfig: mode = " + wviz.env.transitType + " date = " + wviz.env.Date + " time = " + wviz.env.Time);
// Pull data out of the config
transitInfo.mode = null;
switch (wviz.env.transitType)
{
case "Walk":
transitInfo.mode = "walking";
break;
case "Bike":
transitInfo.mode = "biking";
break;
}
switch (wviz.env.busSubway)
{
case "No":
break;
case "Yes":
transitInfo.mode += ",bus";
break;
}
switch (wviz.env.regionalRail)
{
case "No":
break;
case "Yes":
transitInfo.mode += ",train";
break;
}
transitInfo.date = null;
switch (wviz.env.Date)
{
case "Weekday":
transitInfo.date = "weekday";
break;
case "Saturday":
transitInfo.date = "saturday";
break;
case "Sunday":
transitInfo.date = "sunday";
break;
}
vals = wviz.env.Time.split(":");
transitInfo.time = (vals[0] * 60 + +vals[1]) * 60 + +vals[2];
// Change or setup the transportation layer
resetTransLayer();
}
// We need to set the globe or map type here before anything gets run
wviz.settings = {
"map type":"map2d",
"start":{
"lon":-75.20,
"lat":40.02,
"height":0.01
},
"info url":""
};
// Let the startup routine know we're happy
true;
view raw gistfile1.txt hosted with ❤ by GitHub

It's incredibly flexible and as long as it's not in the main rendering loop (good grief no!) still very fast.  There are other advantages too.

Everything Speaks JSON


These days every remote web service speaks JSON.  [Except for the ones that don't].  Using Javascript makes it much easier to decode a random JSON return value and do something visual with it.

SF Bay Area Bike Share

For example, several public bike share companies provide JSON feeds with up-to-the-minute station availability.  They have spatial data, but not as GeoJSON.  That's kind of annoying, but now easily fixed.
  • We fetch the appropriate feed in the script
  • Parse the JSON return and iterate through the records
  • Pull out location data and anything we might want to display
  • Convert the data to GeoJSON and hand it back to WhirlyViz

WhirlyViz still knows nothing about the JSON feeds from the bike share companies, it's just displaying GeoJSON with some styling info.  The script has all the smarts and, best of all, it wasn't all that much code.

Running Your Own Script


WhirlyViz has its own URL scheme for displaying data.  Refer back to this post for details.

Now you can run your own Javascript configuration file too.  Just specify it like so.

whirlyviz://script?js=http://foo.bar.com/myscript.js
view raw gistfile1.txt hosted with ❤ by GitHub

The script argument tells WhirlyViz we're running a script and the js argument tells it where to go get it.  Of course, then you have to write one of these and the documentation is... lacking.

Wrapup


You can see some of the new examples in the latest WhirlyViz update.  I'm now talking to two new geospatial data services which will get their own blog posts.

I'm very happy with this approach and I'll be writing up the Javascript functions shortly.  You can kind of figure it out from the examples, but documentation is important.

In the mean time, if you have a neat visualization you'd like to see on mobile, hit me up and we'll talk.

No comments:

Post a Comment