WebGL with a little help from Babylon.js

BabylonFig1
Most modern browsers now support HTML5 WebGL standard: Internet Explorer 11+, Firefox 4+, Google Chrome 9+, Opera 12+
One of the latest to the party is IE 11.

BabylonFig2

Fig 2 – html5 test site showing WebGL support for IE11

WebGL support means that GPU power is available to javascript developers in supporting browsers. GPU technology fuels the $46.5 billion “vicarious life” industry. Video gaming revenues surpass even Hollywood movie tickets in annual revenues, but this projection shows a falling revenue curve by 2019. Hard to say why the decline, but is it possibly an economic side effect of too much vicarious living? The relative merits of passive versus active forms of “vicarious living” are debatable, but as long as technology chases these vast sums of money, GPU geometry pipeline performance will continue to improve year over year.

WebGL exposes immediate mode graphics pipelines for fast 3D transforms, lighting, shading, animations, and other amazing stuff. GPU induced endorphin bursts do have their social consequences. Apparently, Huxley’s futuristic vision has won out over Orwell’s, at least in internet culture.

“In short, Orwell feared that what we fear will ruin us. Huxley feared that our desire will ruin us.”

Neil Postman Amusing Ourselves to Death.

Aside from the Soma like addictive qualities of game playing, game creation is actually a lot of work. Setting up WebGL scenes with objects, textures, shaders, transforms … is not a trivial task, which is where Dave Catuhe’s Babylon.js framework comes in. Dave has been building 3D engines for a long time. In fact I’ve played with some of Dave’s earlier efforts in Ye olde Silverlight days of yore.

“I am a real fan of 3D development. Since I was 16, I spent all my spare time creating 3d engines with various technologies (DirectX, OpenGL, Silverlight 5, pure software, etc.). My happiness was complete when I discovered that Internet Explorer 11 has native support for WebGL. So I decided to write once again a new 3D engine but this time using WebGL and my beloved JavaScript.”

Dave Catuhe Eternal Coding

Dave’s efforts improve with each iteration and Babylon.js is a wonderfully powerful yet simple to use javascript WebGL engine. The usefulness/complexity curve is a rising trend. To be sure a full fledged gaming environment is still a lot of work. With babylon.js much of the heavy lifting falls to the art design guys. From a mapping perspective I’m happy to forego the gaming, but still enjoy some impressive 3D map building with low effort.

In order to try out babylon.js I went back to an old standby, NASA Earth Observation data. NASA has kindly provided an OGC WMS server for their earth data. Brushing off some old code I made use of babylon.js to display NEO data on a rotating globe.

Babylon.js has innumerable samples and tutorials which makes learning easy for those of us less inclined to read manuals. This playground is an easy way to experiment: Babylon playground

Babylon.js engine is used to create a scene which is then handed off to engine.runRenderLoop. From a mapping perspective, most of the interesting stuff happens in createScene.

Here is a very basic globe:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Babylon.js Globe</title>

    <script src="http://www.babylonjs.com/babylon.js"></script>
    <style>
        html, body {
            overflow: hidden;
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }

        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>

</head>
<body>
    <canvas id="renderCanvas"></canvas>

    <script>
        var canvas = document.getElementById("renderCanvas");
        var engine = new BABYLON.Engine(canvas, true);

        var createScene = function () {
            var scene = new BABYLON.Scene(engine);

            // Light
            var light = new BABYLON.HemisphericLight("HemiLight", new BABYLON.Vector3(-2, 0, 0), scene);

            // Camera
            var camera = new BABYLON.ArcRotateCamera("Camera", -1.57, 1.0, 200, new BABYLON.Vector3.Zero(), scene);
            camera.attachControl(canvas);

            //Creation of a sphere
            //(name of the sphere, segments, diameter, scene)
            var sphere = BABYLON.Mesh.CreateSphere("sphere", 100.0, 100.0, scene);
            sphere.position = new BABYLON.Vector3(0, 0, 0);
            sphere.rotation.x = Math.PI;

            //Add material to sphere
            var groundMaterial = new BABYLON.StandardMaterial("mat", scene);
            groundMaterial.diffuseTexture = new BABYLON.Texture('textures/earth2.jpg', scene);
            sphere.material = groundMaterial;

            // Animations - rotate earth
            var alpha = 0;
            scene.beforeRender = function () {
                sphere.rotation.y = alpha;
                alpha -= 0.01;
            };

            return scene;
        }

        var scene = createScene();

        // Register a render loop to repeatedly render the scene
        engine.runRenderLoop(function () {
            scene.render();
        });

        // Watch for browser/canvas resize events
        window.addEventListener("resize", function () {
            engine.resize();
        });
    </script>
</body>
</html>


Fig 3- rotating Babylon.js globe

Add one line for a 3D effect using a normal (bump) map texture.

groundMaterial.bumpTexture = new BABYLON.Texture('textures/earthnormal2.jpg', scene);


Fig 4 – rotating Babylon.js globe with normal (bump) map texture

The textures applied to BABYLON.Mesh.CreateSphere required some transforms to map correctly.

BabylonFig5

Fig 5 – texture images require img.RotateFlip(RotateFlipType.Rotate90FlipY);

Without this image transform the resulting globe is more than a bit warped. It reminds me of a pangea timeline gone mad.

BabylonFig6

Fig 6 – globe with no texture image transform


Updating our globe texture skin requires a simple proxy that performs the img.RotateFlip after getting the requested NEO WMS image.

        public Stream GetMapFlip(string wmsurl)
        {
            string message = "";
            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(new Uri(wmsurl));
                using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusDescription.Equals("OK"))
                    {
                        using (Image img = Image.FromStream(response.GetResponseStream()))
                        {
                            //rotate image 90 degrees, flip on Y axis
                            img.RotateFlip(RotateFlipType.Rotate90FlipY);
                            using (MemoryStream memoryStream = new MemoryStream()) {
                                img.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
                                WebOperationContext.Current.OutgoingResponse.ContentType = "image/png";
                                return new MemoryStream(memoryStream.ToArray());
                            }
                        }
                    }
                    else message = response.StatusDescription;
                }
            }
            catch (Exception e)
            {
                message = e.Message;
            }
            ASCIIEncoding encoding = new ASCIIEncoding();
            Byte[] errbytes = encoding.GetBytes("Err: " + message);
            return new MemoryStream(errbytes);
        }

With texture in hand the globe can be updated adding hasAlpha true:

var overlayMaterial = new BABYLON.StandardMaterial("mat0", nasa.scene);
var nasaImageSrc = Constants.ServiceUrlOnline + "/GetMapFlip?url=http://neowms.sci.gsfc.nasa.gov/wms/wms?Service=WMS%26version=1.1.1%26Request=GetMap%26Layers=" + nasa.image + "%26BGCOLOR=0xFFFFFF%26TRANSPARENT=TRUE%26SRS=EPSG:4326%26BBOX=-180.0,-90,180,90%26width=" + nasa.width + "%26height=" + nasa.height + "%26format=image/png%26Exceptions=text/xml";
       overlayMaterial.diffuseTexture = new BABYLON.Texture(nasaImageSrc, nasa.scene);
       overlayMaterial.bumpTexture = new BABYLON.Texture('textures/earthnormal2.jpg', nasa.scene);
       overlayMaterial.diffuseTexture.hasAlpha = true;
       nasa.sphere.material = overlayMaterial;

True hasAlpha lets us show a secondary earth texture through the NEO overlay where data was not collected. For example Bathymetry, GEBCO_BATHY, leaves holes for the continental masses that are transparent making the earth texture underneath visible. Alpha sliders could also be added to stack several NEO layers, but that’s another project.

BabylonFig7

Fig 7 – alpha bathymetry texture over earth texture

Since a rotating globe can be annoying it’s worthwhile adding a toggle switch for the rotation weary. One simple method is to make use of a Babylon pick event:

        window.addEventListener("click", function (evt) {
            var pickResult = nasa.scene.pick(evt.clientX, evt.clientY);
            if (pickResult.pickedMesh.id != "skyBox") {
                if (nasa.rotationRate < 0.0) nasa.rotationRate = 0.0;
                else nasa.rotationRate = -0.005;
            }
        });

In this case any click ray that intersects the globe will toggle globe rotation on and off. Click picking is a kind of collision checking for object intersection in the scene which could be very handy for adding globe interaction. In addition to pickedMesh.id, pickResult gives a pickedPoint location, which could be reverse transformed to a latitude,longitude.

Starbox (no coffee involved) is a quick way to add a surrounding background in 3D. It’s really just a BABYLON.Mesh.CreateBox big enough to engulf the earth sphere, a very limited kind of cosmos. The stars are not astronomically accurate just added for some mood setting.

Another handy BABYLON Feature is BABYLON.Mesh.CreateGroundFromHeightMap

/* Name
 * Height map picture url
 * mesh Width
 * mesh Height
 * Number of subdivisions (increase the complexity of this mesh)
 * Minimum height : The lowest level of the mesh
 * Maximum height : the highest level of the mesh
 * scene
 * Updatable: say if this mesh can be updated dynamically in the future (Boolean)
**/

var height = BABYLON.Mesh.CreateGroundFromHeightMap("height", "textures/" + heightmap, 200, 100, 200, 0, 2, scene, false);

For example using a grayscale elevation image as a HeightMap will add exaggerated elevation values to a ground map:

BabylonFig8

Fig 8 – elevation grayscale jpeg for use in BABYLON HeightMap

BabylonFig9

Fig -9 – HeightMap applied

The HeightMap can be any value for example NEO monthly fires converted to grayscale will show fire density over the surface.

BabylonFig10

Fig 10 – NEO monthly fires as heightmap

In this case a first person shooter, FPS, camera was substituted for a generic ArcRotate Camera so users can stalk around the earth looking at fire spikes.

“FreeCamera – This is a ‘first person shooter’ (FPS) type of camera where you control the camera with the mouse and the cursors keys.”

Lots of camera choices are listed here including Oculus Rift which promises some truly immersive map opportunities. I assume this note indicates Babylon is waiting on the retail release of Oculus to finalize a camera controller.

“The OculusCamera works closely with our Babylon.js OculusController class. More will be written about that, soon, and nearby.

Another Note: In newer versions of Babylon.js, the OculusOrientedCamera constructor is no longer available, nor is its .BuildOculusStereoCamera function. Stay tuned for more information.”

So it may be only a bit longer before “vicarious life” downhill skiing opportunities are added to FreshyMap.

Links:

BabylonFig11

Fig 11 - NEO Land Surface average night temperature

Nokia Here Map Isochrone API

Isochrone Polygon

Isochrones - 5, 10, and 15 minute drive time polygons

One of the newer kids on the web mapping block is Nokia Here Maps. I say “newer” but Nokia is actually also one of the oldest on the block. Nokia purchased NavTeq back in 2008 and merged it into the Nokia fold as Here Maps around 2012. NavTeq had a long history in the digital map era starting back in the mid ‘80s, long before cell phones, as Karlin & Collins.

If you look at data sources in this web map matrix, you’ll notice that NavTeq data is a source for Bing Maps, Yahoo Maps, and MapQuest as well as Nokia Here Maps. In the web map world there are numerous interlocking license arrangements, but NavTeq is a key data piece in some of the most popular web map services.

Nokia, through its NavTeq purchase, has a long history in map data collection and provisioning markets, but a relatively new face in the consumer UI markets. As digital map markets evolve along new vectors like mobile phones, in-dashboard automobile devices, and autonomous robotics, Nokia’s map data is positioned to be a key player even if ultimately Microsoft Nokia phones fall off the map.

Nokia APIs offer features not often found in other web map APIs, including truck restricted routing, multi-mode (transit, car, pedestrian) routing, isoline/isochrone route polygons, multi stop matrix routing, predictive traffic routing, integrated heatmap, integrated point clustering, ….

Isoline Calculations

Isoline route polygons are an interesting addition to the web map tool kit. The result of an isoline query is a set of vertices describing a polygon. This polygon is the outer edge of all possible travel routes from a start point to a given distance.

Distance Isoline

Distance Isoline

Example REST query:
https://route.st.nlp.nokia.com/routing/6.2/calculateisoline.json?mode=fastest;pedestrian;traffic:disabled&start=52.5160,13.3778 &distance=2000&app_id=DemoAppId01082013GAL&app_code=AJKnXv84fjrb0KIHawS0Tg

Result:

{
    "Response":
    {
        "isolines":[
            {
                "name":"Isoline",
                "value":[
                    "52.5151405,13.3487797",
                    "52.5195503,13.3519497",
                    "52.5195503,13.3519497",
                    "52.519371,13.3522596",
                    "52.519371,13.3522596",
			.
			.
			.
                    "52.5030594,13.3613596",
                    "52.5030594,13.3613596",
                    "52.5034218,13.3609695",
                    "52.5034218,13.3609695",
                    "52.5038605,13.3606596",
                    "52.5038605,13.3606596",
                    "52.5061188,13.3556404",
                    "52.5061188,13.3556404",
                    "52.5096283,13.3527203",
                    "52.5096283,13.3527203",
                    "52.5136795,13.3490601",
                    "52.5136795,13.3490601",
                    "52.5151405,13.3487797"
                ],
                "scope":"com.navteq.lbsp.cdm.routing.calculateisoline.CalculateIsolineResponseType",
                "declaredType":"java.util.List",
                "globalScope":false,
                "nil":false,
                "typeSubstituted":true
            }
        ],
        "MetaInfo":
        {
            "Timestamp":"2014-08-25T21:38:58.467Z",
            "AdditionalData":[
                {
                    "value":"2014-08-25T21:38:00.200+0000",
                    "key":"CurrentTrafficLastUpdate"
                },
                {
                    "value":"10949245",
                    "key":"CurrentTrafficElementsCount"
                },
                {
                    "value":"2014-08-25T21:38:00.004+0000",
                    "key":"LongTermClosureLastUpdate"
                },
                {
                    "value":"37696",
                    "key":"LongTermClosureElementsCount"
                },
                {
                    "value":"2014-08-25T21:38:00.004+0000",
                    "key":"ShortTermClosureLastUpdate"
                },
                {
                    "value":"9414",
                    "key":"ShortTermClosureElementsCount"
                },
                {
                    "value":"2014Q1",
                    "key":"Map0"
                },
                {
                    "value":"routeserver,9.3-2014.08.12",
                    "key":"Module0"
                },
                {
                    "value":"63",
                    "key":"Module0ExecTime"
                },
                {
                    "value":"63563",
                    "key":"Module0ExecTimeMicro"
                },
                {
                    "value":"routing-route-service,6.2.37.0",
                    "key":"Service"
                }
            ]
        },
        "Center":
        {
            "Latitude":52.5158615,
            "Longitude":13.3774099
        }
    }
}

Emergency Response Times

Isochrone calculations produce a similar result but using a given time instead of distance.

Isochrone Calculation

Isochrone Calculation

These calculations allow some interesting queries. For example what is the access reach from a fire station for 5, 10, and 15 minute drive time envelopes. These are the types of calculations of interest to insurance companies and fire district chiefs. In addition to general route envelope calculations Here Map also provides traffic enabled predictive isochrones. In other words the envelope calculation with traffic enabled is dependent on traffic patterns at a given time and day. A 10 min drive time reach will be less for weekday rush hour traffic patterns than for evenings or weekends.

Isochrone firestation

Example of a 5, 10, and 15 minute drive time envelope from N Washington Fire Station at departure 11:00AM MDT Denver time.

N Washington Fire Station at departure 5:00PM MDT rush-hour overlaid on 8:00PM MDT after rush-hour. The furthest extents are the non-rush-hour isochrones. Predictive traffic routing can be useful in urban areas where rush-hour variation is significant.

Firestation Isochrone

Compared departure times - rush hour versus evening predictive traffic

Colorado Springs, CO has nearly complete 15min coverage as seen from the selection of all fire station locations. For station location planning Isochrone calculations can provide a quick first pass for coverage estimates.

Colorado Springs, CO Firestation coverage

Firestation 5, 10, 15 min coverage

This simple example uses Here Map nokia.maps.search.Manager to geocode an address text. After zooming and centering the map, this geocoded location is then passed to a REST call to search for the term “fire station”.

url: "http://places.cit.api.here.com/places/v1/discover/search?at=" +
center.latitude + "," + center.longitude +
"&q=fire station&app_id=DemoAppId01082013GAL
&app_code=AJKnXv84fjrb0KIHawS0Tg&accept=application/json"

Since nokia.maps.advrouting.Manager with Isochrone is part of Here Enterprise javascript, it’s easier to use the REST interface for find places search rather than try to untangle Here standard javascript API and the Here enterprise javascript API. The results of the “fire station” place search are added as pins to the map. The pins include a click listener that creates an infobubble and then sets up isochrone routingRequests for the 5, 10, and 15 minute isochrones.

Commuter Isochrone

Nokia also provides inverse Reverse-Flow calculations showing the edge of all routes that can be used to reach a point in a given distance or time.

ReverseFlow Distance

Distance Based Reverse Flow

Time Based Reverse Flow

Time Based Reverse Flow

If you’d like to determine the neighborhoods within a certain commute time of your work these calculations can also come in handy. Before looking for an apartment in a new city, it might be nice to see the neighborhoods within a 15 or 30 min drive time.

Time-based Reverse Flow calculations with traffic enabled and departure at 7:30AM would give you an idea of “to” work limits. The opposite Isochrone calculation at 5:30PM would provide commute neighborhoods in the return home 15 min envelope.

Sample Time-based Reverse Flow calculation to work at 7:30AM MDT:
https://route.st.nlp.nokia.com/routing/6.2/reverseflow.json?mode=fastest;car;traffic:enabled&destination=39.744128,-104.985839&departure=2014-08-25T13:30:00Z&time=PT0H10M&app_id=DemoAppId01082013GAL&app_code=AJKnXv84fjrb0KIHawS0Tg

Unfortunately the maximum allowable reverse flow calculation is currently 10 minutes, PT0H10M, which limits usefulness for a commute range calculation. The next image shows a set of reachable street segment links for a 10min Reverse Time Flow to a Colorado Springs, CO destination.

Reverse flow

10min Time Based Reverse Flow

Sample REST service 30 minute Isochrone calculation from work at 5:30PM MDT = 23:30 UTC:
https://route.st.nlp.nokia.com/routing/6.2/calculateisoline.json?mode=fastest;car;traffic:enabled&start=39.744128,-104.985839&departure=2014-08-25T23:30:00Z&time=PT0H30M&app_id=DemoAppId01082013GAL&app_code=AJKnXv84fjrb0KIHawS0Tg

Isochrone route calculations are just one of the many advanced tools found in Nokia Here Map APIs.

Here Explorer: https://developer.here.com/javascript-apis/enterprise-api-explorer