Menu

Full Javascript Implementation

2017-05-19
2020-10-01
  • William Wall

    William Wall - 2017-05-19

    I noticed that the Javascript port included in GeographicLib only contains the geodesic calculations. I was also interested in the rhumb versions along with the MGRS conversion functions. I initially went down the route of attempting to flesh out the Javascript port, but quickly realized that it was a non-trivial task to bring it up to speed.

    Queue shower thought: Didn't someone compile the Unreal Engine and DOOM to Javascript?

    That got me to try running the original C++ implementation through emscripten to compile Web Assembly and asm.js modules. Worked like a charm and the results significantly outperform a pure JS implementation!

    Thanks for such an excellent library!

     
  • Charles Karney

    Charles Karney - 2017-05-19

    Perhaps you can post step-by-step instructions of how to do this...? Thanks.

     
  • William Wall

    William Wall - 2017-05-22

    I was going to link the project as soon as it goes open source, which should be a matter of days now (silly lawyers).

    In the meantime, here's the gist:

    1. Follow emscripten's setup instructions to get the environment set up
    2. Make a bindings.cpp file
    #include <emscripten/bind.h>
    #include <GeographicLib/Geodesic.hpp>
    #include <GeographicLib/Rhumb.hpp>
    
    using namespace GeographicLib;
    using namespace emscripten;
    
    struct Coord {
      double lon;
      double lat;
    };
    
    struct GeodesicDirectResult {
      double distance;
      double initialBearing;
      double finalBearing;
    };
    
    struct RhumbDirectResult {
      double distance;
      double bearing;
    };
    
    // Geodesic
    GeodesicDirectResult geodesicInverse(Coord p1, Coord p2) {
      const Geodesic& geodesic = Geodesic::WGS84();
      double s12, azi1, azi2;
      geodesic.Inverse(p1.lat, p1.lon, p2.lat, p2.lon, s12, azi1, azi2);
      return GeodesicDirectResult {s12, azi1, azi2};
    }
    
    Coord geodesicDirect(Coord p1, double azi1, double s12) {
      const Geodesic& geodesic = Geodesic::WGS84();
      double lon2, lat2;
      geodesic.Direct(p1.lat, p1.lon, azi1, s12, lat2, lon2);
      return Coord {lon2, lat2};
    }
    
    // Rhumb
    RhumbDirectResult rhumbInverse(Coord p1, Coord p2) {
      const Rhumb& rhumb = Rhumb::WGS84();
      double s12, azi1;
      rhumb.Inverse(p1.lat, p1.lon, p2.lat, p2.lon, s12, azi1);
      return RhumbDirectResult {s12, azi1};
    }
    
    Coord rhumbDirect(Coord p1, double azi1, double s12) {
      const Rhumb& rhumb = Rhumb::WGS84();
      double lon2, lat2;
      rhumb.Direct(p1.lat, p1.lon, azi1, s12, lat2, lon2);
      return Coord {lon2, lat2};
    }
    
    // see https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html#embind
    EMSCRIPTEN_BINDINGS(GeographicLib) {
      // map Coord struct to an array
      value_array<Coord>("Coord")
        .element(&Coord::lon)
        .element(&Coord::lat);
    
      // map Result struct to an object
      value_object<GeodesicDirectResult>("GeodesicDirectResult")
        .field("distance", &GeodesicDirectResult::distance)
        .field("initialBearing", &GeodesicDirectResult::initialBearing)
        .field("finalBearing", &GeodesicDirectResult::finalBearing);
    
      value_object<RhumbDirectResult>("RhumbDirectResult")
        .field("distance", &RhumbDirectResult::distance)
        .field("bearing", &RhumbDirectResult::bearing);
    
      function("geodesicDirect", &geodesicDirect);
      function("geodesicInverse", &geodesicInverse);
      function("rhumbDirect", &rhumbDirect);
      function("rhumbInverse", &rhumbInverse);
    }
    

    Then run the following command to build it

    emcc --bind -O3 -Ipath/to/geographiclib/include/ -s WASM=1 path/to/geographiclib/*.cpp bindings.cpp
    

    The emscripten bind docs have way more options for the bindings. I only needed the calculations for the WGS84 ellipsoid, but you could potentially expose the full classes with any of their methods for a more generic and full-featured build.

    To test it out just load it up the resulting script file in an HTML page and call any method you like with Module.geodesicDirect(...).

    To generate asm.js instead of WebAssembly (which is very new), just drop the -s WASM=1 flag. I was able to squeeze out more performance by setting -D GEOGRPHICLIB_PRECISION=1 and replacing double with float in the bindings.cpp file (anything beyond 1m precision is overkill for us).

    My tests show the WebAssembly version (with float rather than double) runs in roughly a third of the time of the pure JS library available in GeographicLib. The asm.js version is not quite as fast but has the advantage of being compatible with browsers that do not support asm.js (it'll just run slower in that case).

     

    Last edit: William Wall 2017-05-22
  • Charles Karney

    Charles Karney - 2017-05-22

    Many thanks. I'll go through the steps myself when I get a chance. I don't understand how you're able to get better performance with floats instead of doubles. Don't these both map to the same basic number (double precision) in Javascript?

     
  • William Wall

    William Wall - 2017-05-22

    In Javascript, yes. However, the WebAssembly module is downloaded and converted directly to native instructions by the browser. The functions in that bindings.cpp file are not run in a Javascript context, but in a native one. The results, when passed back to Javascript, are definitely in Javascript's double-precision number format.

    At least that's my understanding. This is all new for me. If you still expect it to run natively in the same time, then perhaps it is a deficiency in the compile step which creates the WebAssembly module. I know LLVM is working on WebAssembly as a build target, but I believe it is still experimental. Emscripten currently uses a project called "binaryen" to build it.

     
  • William Wall

    William Wall - 2017-12-07

    I completely underestimated how much red tape there was to get the project open sourced.

    Better late than never: https://github.com/ngageoint/opensphere-asm

     
    • Anonymous

      Anonymous - 2020-09-30

      Hi William,

      I appreciate the work you did.

      I'm trying to use your project and need to expose the function toMGRS or any other function.

      emcc --bind -O3 -I$GEO_PATH/include/ -s EXPORTED_FUNCTIONS="['_geodesicDirect']" src/bindings.cpp $GEO_PATH/src/*.cpp

      but it cannot be found.

      The reason I need this is becouse I use it in non browser environment like this:
      it should be like this: exports._geodesicDirect

      Here is the environment:

      --
      async function main() {
      const wa = await WebAssembly.instantiate(bytes, imports);
      const exports = wa.instance.exports;
      const magic_func = exports._geodesicDirect;
      return x.map((val) => {
      return magic_sum(val, val);
      });
      }
      --

       

Anonymous
Anonymous

Add attachments
Cancel