From 50c6e0e5e456423b2dc4911bf260c2e665a13483 Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Wed, 7 Oct 2026 06:54:59 +0200 Subject: [PATCH 1/3] feat(demo): interactive WebAssembly demo on a Leaflet map demo/: embind wrapper over geo-utils-cpp, seven interactive panels (distance & bearing, offset, geofence, polyline encode/decode p5/p6, simplify, snap-to-route, area), a native reference + Node cross-check, and a Pages workflow. Not built or verified against WASM yet (needs Emscripten). --- .github/workflows/demo.yml | 61 +++++++ .gitignore | 4 + demo/CMakeLists.txt | 29 +++ demo/README.md | 50 ++++++ demo/bindings.cpp | 60 +++++++ demo/build.sh | 14 ++ demo/demo_api.hpp | 124 +++++++++++++ demo/test/native_ref.cpp | 54 ++++++ demo/test/verify.mjs | 52 ++++++ demo/web/index.html | 360 +++++++++++++++++++++++++++++++++++++ 10 files changed, 808 insertions(+) create mode 100644 .github/workflows/demo.yml create mode 100644 demo/CMakeLists.txt create mode 100644 demo/README.md create mode 100644 demo/bindings.cpp create mode 100755 demo/build.sh create mode 100644 demo/demo_api.hpp create mode 100644 demo/test/native_ref.cpp create mode 100644 demo/test/verify.mjs create mode 100644 demo/web/index.html diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml new file mode 100644 index 0000000..19bd56f --- /dev/null +++ b/.github/workflows/demo.yml @@ -0,0 +1,61 @@ +name: Demo (GitHub Pages) + +on: + push: + branches: [master] + paths: ['demo/**', 'include/**', '.github/workflows/demo.yml'] + pull_request: + paths: ['demo/**', 'include/**', '.github/workflows/demo.yml'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: mymindstorm/setup-emsdk@v16 + with: + version: 6.0.11 + - uses: actions/setup-node@v7 + with: + node-version: 22 + + - name: Build native reference + run: | + c++ -std=c++17 -Wall -Wextra -pedantic -Iinclude demo/test/native_ref.cpp -o native_ref + ./native_ref > expected.json + + - name: Build WebAssembly + run: demo/build.sh + + - name: Cross-check WASM against native + run: node demo/test/verify.mjs expected.json + + - name: Report size + run: ls -l demo/web/geo.wasm demo/web/geo.js + + - uses: actions/upload-pages-artifact@v5 + if: github.event_name != 'pull_request' + with: + path: demo/web + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 885fb4a..dd8f850 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ Thumbs.db # Tool sessions .claude/ + +# WebAssembly demo build output (see demo/README.md) +demo/web/geo.js +demo/web/geo.wasm diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt new file mode 100644 index 0000000..a1fd4c2 --- /dev/null +++ b/demo/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) +project(geo_utils_cpp_demo LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(GEO_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../include") + +if(EMSCRIPTEN) + # WebAssembly module: web/geo.js + web/geo.wasm + add_executable(geo bindings.cpp) + target_include_directories(geo PRIVATE "${GEO_INCLUDE_DIR}") + target_compile_options(geo PRIVATE -O3 -Wall -Wextra) + target_link_options(geo PRIVATE + --bind -O3 + -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createGeo + -sALLOW_MEMORY_GROWTH=1 + -sENVIRONMENT=web,node + -sASSERTIONS=0) + set_target_properties(geo PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/web" + SUFFIX ".js") +else() + # Native reference used to cross-check the WebAssembly build (test/verify.mjs). + add_executable(native_ref test/native_ref.cpp) + target_include_directories(native_ref PRIVATE "${GEO_INCLUDE_DIR}") + target_compile_options(native_ref PRIVATE -Wall -Wextra -pedantic) +endif() diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..9c739dd --- /dev/null +++ b/demo/README.md @@ -0,0 +1,50 @@ +# geo-utils-cpp — WebAssembly demo + +An interactive map page where every number is computed by the C++ library compiled to +WebAssembly: distance & bearing, offset, geofence (point-in-polygon), polyline +encode/decode (precision 5 and 6), Douglas–Peucker simplify, snap-to-route, and spherical area. +Client-side only: no backend, no API keys, no trackers. Map tiles come from OpenStreetMap +and Leaflet from unpkg. + +## Layout + +| File | Purpose | +|---|---| +| `demo_api.hpp` | Thin C++17 layer over ``; paths are flat `[lat, lng, ...]` arrays. No Emscripten dependency. | +| `bindings.cpp` | embind wrapper — one forwarding function per `demo_api.hpp` function. | +| `test/native_ref.cpp` | Native build of the same layer; prints reference values as JSON. | +| `test/verify.mjs` | Loads the WASM module in Node and compares it to the native reference. | +| `web/index.html` | The page (vanilla JS + Leaflet). | + +## Build + +Needs the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) +(`emcmake` on `PATH`, e.g. after `source ./emsdk_env.sh`) and CMake ≥ 3.16. + +```sh +demo/build.sh # -> demo/web/geo.js + demo/web/geo.wasm +ls -l demo/web/geo.wasm +``` + +Build flags: `-O3 --bind -sMODULARIZE -sEXPORT_ES6 -sALLOW_MEMORY_GROWTH`. + +## Run locally + +```sh +cd demo/web && python3 -m http.server 8000 # open http://localhost:8000 +``` + +Browsers refuse to load a `.wasm` module from `file://`, so opening `index.html` directly shows +an explanatory message instead of the demo. Any static server works. + +## Verify the WASM build against native + +```sh +c++ -std=c++17 -Iinclude demo/test/native_ref.cpp -o native_ref && ./native_ref > expected.json +node demo/test/verify.mjs expected.json +``` + +## Publish on GitHub Pages + +`.github/workflows/demo.yml` builds, verifies and deploys `demo/web` on pushes to `master`. +One-time setup: **Settings → Pages → Build and deployment → Source: GitHub Actions**. diff --git a/demo/bindings.cpp b/demo/bindings.cpp new file mode 100644 index 0000000..52a1efe --- /dev/null +++ b/demo/bindings.cpp @@ -0,0 +1,60 @@ +// Emscripten/embind wrapper. Arrays cross the boundary as plain JS arrays of numbers +// (flat [lat, lng, lat, lng, ...]); every function is a thin forward to demo_api.hpp. +#include "demo_api.hpp" + +#include +#include + +#include +#include + +namespace { + +using emscripten::val; + +demo::Flat in(const val& array) { + return emscripten::convertJSArrayToNumberVector(array); +} + +val out(const demo::Flat& flat) { + val array = val::array(); + for (std::size_t i = 0; i < flat.size(); ++i) { + array.set(i, flat[i]); + } + return array; +} + +} // namespace + +EMSCRIPTEN_BINDINGS(geo_demo) { + using namespace emscripten; + + function("version", &demo::version); + function("distanceBetween", &demo::distance_between); + function("heading", &demo::heading); + function("offset", +[](double lat, double lng, double d, double h) { return out(demo::offset(lat, lng, d, h)); }); + function("interpolate", +[](double a, double b, double c, double d, double f) { + return out(demo::interpolate(a, b, c, d, f)); + }); + function("pathLength", +[](const val& path) { return demo::path_length(in(path)); }); + function("pointAtDistance", +[](const val& path, double d) { return out(demo::point_at_distance(in(path), d)); }); + function("area", +[](const val& path) { return demo::area(in(path)); }); + function("contains", +[](double lat, double lng, const val& poly, bool geodesic) { + return demo::contains(lat, lng, in(poly), geodesic); + }); + function("onEdge", +[](double lat, double lng, const val& poly, bool geodesic, double tol) { + return demo::on_edge(lat, lng, in(poly), geodesic, tol); + }); + function("distanceToEdge", +[](double lat, double lng, const val& poly) { + return demo::distance_to_edge(lat, lng, in(poly)); + }); + function("encode", +[](const val& path, unsigned precision) { return demo::encode(in(path), precision); }); + function("decode", +[](const std::string& s, unsigned precision) { return out(demo::decode(s, precision)); }); + function("simplify", +[](const val& path, double tol, bool geodesic) { + return out(demo::simplify(in(path), tol, geodesic)); + }); + function("closestPointOnPath", +[](double lat, double lng, const val& path) { + return out(demo::closest_point_on_path(lat, lng, in(path))); + }); + function("bounds", +[](const val& path) { return out(demo::bounds(in(path))); }); +} diff --git a/demo/build.sh b/demo/build.sh new file mode 100755 index 0000000..b685b6e --- /dev/null +++ b/demo/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Builds web/geo.js + web/geo.wasm. Requires the Emscripten SDK (emcmake on PATH). +set -euo pipefail +cd "$(dirname "$0")" + +if ! command -v emcmake >/dev/null 2>&1; then + echo "emcmake not found. Install and activate the Emscripten SDK first:" >&2 + echo " https://emscripten.org/docs/getting_started/downloads.html (source ./emsdk_env.sh)" >&2 + exit 1 +fi + +emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release +cmake --build build-wasm --parallel +ls -l web/geo.js web/geo.wasm diff --git a/demo/demo_api.hpp b/demo/demo_api.hpp new file mode 100644 index 0000000..4c85ac1 --- /dev/null +++ b/demo/demo_api.hpp @@ -0,0 +1,124 @@ +// Thin, JS-friendly layer over geo-utils-cpp. Paths are flat arrays +// [lat0, lng0, lat1, lng1, ...]. Pure C++17 with no Emscripten dependency, so the +// exact same code is compiled natively (test/native_ref.cpp) to cross-check the +// WebAssembly build. +#pragma once + +#include + +#include +#include +#include +#include + +namespace demo { + +using Flat = std::vector; + +inline std::vector to_path(const Flat& flat) { + std::vector path; + path.reserve(flat.size() / 2); + for (std::size_t i = 0; i + 1 < flat.size(); i += 2) { + path.emplace_back(flat[i], flat[i + 1]); + } + return path; +} + +inline Flat to_flat(const std::vector& path) { + Flat flat; + flat.reserve(path.size() * 2); + for (const auto& p : path) { + flat.push_back(p.lat); + flat.push_back(p.lng); + } + return flat; +} + +inline std::string version() { return GEO_UTILS_CPP_VERSION_STRING; } + +inline double distance_between(double lat1, double lng1, double lat2, double lng2) { + return geo::distance_between({lat1, lng1}, {lat2, lng2}); +} + +inline double heading(double lat1, double lng1, double lat2, double lng2) { + return geo::heading({lat1, lng1}, {lat2, lng2}); +} + +inline Flat offset(double lat, double lng, double distance, double heading_deg) { + const geo::LatLng p = geo::offset({lat, lng}, distance, heading_deg); + return {p.lat, p.lng}; +} + +inline Flat interpolate(double lat1, double lng1, double lat2, double lng2, double fraction) { + const geo::LatLng p = geo::interpolate({lat1, lng1}, {lat2, lng2}, fraction); + return {p.lat, p.lng}; +} + +inline double path_length(const Flat& flat) { return geo::path_length(to_path(flat)); } + +// Empty result for an empty path. +inline Flat point_at_distance(const Flat& flat, double distance) { + const auto p = geo::point_at_distance(to_path(flat), distance); + return p ? Flat{p->lat, p->lng} : Flat{}; +} + +inline double area(const Flat& flat) { return geo::area(to_path(flat)); } + +inline bool contains(double lat, double lng, const Flat& polygon, bool geodesic) { + return geo::contains({lat, lng}, to_path(polygon), geodesic); +} + +inline bool on_edge(double lat, double lng, const Flat& polygon, bool geodesic, double tolerance) { + return geo::on_edge({lat, lng}, to_path(polygon), geodesic, tolerance); +} + +// Distance in meters from the point to the closest polygon edge (the polygon is +// closed implicitly). -1 for an empty polygon. +inline double distance_to_edge(double lat, double lng, const Flat& polygon) { + auto path = to_path(polygon); + if (path.empty()) { + return -1.0; + } + if (path.size() > 1) { + path.push_back(path.front()); + } + const auto projection = geo::closest_point_on_path({lat, lng}, path); + return projection ? projection->distance : -1.0; +} + +inline std::string encode(const Flat& flat, unsigned precision) { + return geo::encode(to_path(flat), precision); +} + +inline Flat decode(const std::string& encoded, unsigned precision) { + return to_flat(geo::decode(encoded, precision)); +} + +inline Flat simplify(const Flat& flat, double tolerance, bool geodesic) { + return to_flat(geo::simplify(to_path(flat), tolerance, geodesic)); +} + +// [lat, lng, segment, distance_to_path_m, distance_along_path_m]; empty for an empty path. +inline Flat closest_point_on_path(double lat, double lng, const Flat& flat) { + const auto path = to_path(flat); + const auto projection = geo::closest_point_on_path({lat, lng}, path); + if (!projection) { + return {}; + } + const std::vector head(path.begin(), path.begin() + static_cast(projection->segment) + 1); + const double along = geo::path_length(head) + geo::distance_between(path[projection->segment], projection->point); + return {projection->point.lat, projection->point.lng, static_cast(projection->segment), + projection->distance, along}; +} + +// [south, west, north, east]; empty for an empty path. Antimeridian-aware: west > east +// means the box crosses the 180th meridian. +inline Flat bounds(const Flat& flat) { + const auto b = geo::bounds(to_path(flat)); + if (!b) { + return {}; + } + return {b->southwest.lat, b->southwest.lng, b->northeast.lat, b->northeast.lng}; +} + +} // namespace demo diff --git a/demo/test/native_ref.cpp b/demo/test/native_ref.cpp new file mode 100644 index 0000000..556ba74 --- /dev/null +++ b/demo/test/native_ref.cpp @@ -0,0 +1,54 @@ +// Native reference: runs a fixed set of inputs through the same demo_api.hpp that the +// WASM build wraps, and prints them as JSON. test/verify.mjs feeds the identical inputs to +// the WebAssembly module and compares. +#include "../demo_api.hpp" + +#include +#include + +namespace { + +const demo::Flat kRoute = {37.7749, -122.4194, 37.8044, -122.2712, 37.8716, -122.2727, 38.5816, -121.4944}; +const demo::Flat kPolygon = {37.80, -122.45, 37.80, -122.40, 37.76, -122.40, 37.76, -122.45}; +const demo::Flat kAnti = {-17.0, 179.5, -17.5, -179.5, -18.0, 179.8}; +const std::string kPoly5 = "_p~iF~ps|U_ulLnnqC_mqNvxq`@"; // Google's documentation example + +void print_array(const char* key, const demo::Flat& v, bool comma = true) { + std::printf(" \"%s\": [", key); + for (std::size_t i = 0; i < v.size(); ++i) { + std::printf("%s%.17g", i ? ", " : "", v[i]); + } + std::printf("]%s\n", comma ? "," : ""); +} + +void print_number(const char* key, double v, bool comma = true) { + std::printf(" \"%s\": %.17g%s\n", key, v, comma ? "," : ""); +} + +} // namespace + +int main() { + std::printf("{\n"); + std::printf(" \"version\": \"%s\",\n", demo::version().c_str()); + print_number("distance_sf_ny", demo::distance_between(37.7749, -122.4194, 40.7128, -74.0060)); + print_number("heading_sf_ny", demo::heading(37.7749, -122.4194, 40.7128, -74.0060)); + print_array("offset", demo::offset(37.7749, -122.4194, 10000.0, 45.0)); + print_array("interpolate", demo::interpolate(37.7749, -122.4194, 40.7128, -74.0060, 0.25)); + print_number("path_length", demo::path_length(kRoute)); + print_array("point_at_distance", demo::point_at_distance(kRoute, 20000.0)); + print_number("area_polygon", demo::area(kPolygon)); + print_number("contains_inside", demo::contains(37.78, -122.42, kPolygon, false)); + print_number("contains_outside", demo::contains(37.90, -122.42, kPolygon, false)); + print_number("distance_to_edge", demo::distance_to_edge(37.78, -122.42, kPolygon)); + std::printf(" \"encode5\": \"%s\",\n", demo::encode(kRoute, 5).c_str()); + std::printf(" \"encode6\": \"%s\",\n", demo::encode(kRoute, 6).c_str()); + print_array("decode5", demo::decode(kPoly5, 5)); + print_array("decode6_roundtrip", demo::decode(demo::encode(kRoute, 6), 6)); + print_array("simplify_100m", demo::simplify(kRoute, 100.0, false)); + print_array("closest", demo::closest_point_on_path(37.83, -122.30, kRoute)); + print_array("bounds_anti", demo::bounds(kAnti)); + print_number("path_length_anti", demo::path_length(kAnti)); + print_number("area_anti", demo::area(kAnti), false); + std::printf("}\n"); + return 0; +} diff --git a/demo/test/verify.mjs b/demo/test/verify.mjs new file mode 100644 index 0000000..44eebb4 --- /dev/null +++ b/demo/test/verify.mjs @@ -0,0 +1,52 @@ +// Cross-checks the WebAssembly build against the native reference. +// 1. c++ -std=c++17 -I../include test/native_ref.cpp -o native_ref && ./native_ref > expected.json +// 2. node test/verify.mjs expected.json +// Same inputs as native_ref.cpp; exits non-zero on any mismatch. +import { readFileSync } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import path from 'node:path'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const expected = JSON.parse(readFileSync(process.argv[2], 'utf8')); +const { default: createGeo } = await import(pathToFileURL(path.join(here, '../web/geo.js')).href); +const g = await createGeo(); + +const route = [37.7749, -122.4194, 37.8044, -122.2712, 37.8716, -122.2727, 38.5816, -121.4944]; +const polygon = [37.80, -122.45, 37.80, -122.40, 37.76, -122.40, 37.76, -122.45]; +const anti = [-17.0, 179.5, -17.5, -179.5, -18.0, 179.8]; + +const actual = { + version: g.version(), + distance_sf_ny: g.distanceBetween(37.7749, -122.4194, 40.7128, -74.0060), + heading_sf_ny: g.heading(37.7749, -122.4194, 40.7128, -74.0060), + offset: g.offset(37.7749, -122.4194, 10000, 45), + interpolate: g.interpolate(37.7749, -122.4194, 40.7128, -74.0060, 0.25), + path_length: g.pathLength(route), + point_at_distance: g.pointAtDistance(route, 20000), + area_polygon: g.area(polygon), + contains_inside: +g.contains(37.78, -122.42, polygon, false), + contains_outside: +g.contains(37.90, -122.42, polygon, false), + distance_to_edge: g.distanceToEdge(37.78, -122.42, polygon), + encode5: g.encode(route, 5), + encode6: g.encode(route, 6), + decode5: g.decode('_p~iF~ps|U_ulLnnqC_mqNvxq`@', 5), + decode6_roundtrip: g.decode(g.encode(route, 6), 6), + simplify_100m: g.simplify(route, 100, false), + closest: g.closestPointOnPath(37.83, -122.30, route), + bounds_anti: g.bounds(anti), + path_length_anti: g.pathLength(anti), + area_anti: g.area(anti), +}; + +let bad = 0; +for (const [key, want] of Object.entries(expected)) { + const got = actual[key]; + const a = Array.isArray(want) ? want : [want]; + const b = Array.isArray(got) ? got : [got]; + const diff = typeof want === 'string' ? (want === got ? 0 : Infinity) + : Math.max(a.length === b.length ? 0 : Infinity, ...a.map((x, i) => Math.abs(x - b[i]))); + const ok = diff <= 1e-9 * Math.max(1, ...a.map(Math.abs)); + if (!ok) bad++; + console.log(`${ok ? 'OK ' : 'FAIL'} ${key.padEnd(20)} max|Δ| = ${diff}`); +} +process.exit(bad ? 1 : 0); diff --git a/demo/web/index.html b/demo/web/index.html new file mode 100644 index 0000000..69d9bed --- /dev/null +++ b/demo/web/index.html @@ -0,0 +1,360 @@ + + + + + +geo-utils-cpp — live demo + + + + + +
Loading WebAssembly…
+ +
+

geo-utils-cpp

+

Header-only C++17 geodesy, compiled to WebAssembly. Every number below is computed by the C++ library in your browser — no server, no tracking. GitHub ·

+
+ +
+
+ +
+
Map © OpenStreetMap contributors. Distances are on a sphere of radius 6 371 009 m, like Google Maps geometry.
+ + + + + From e5d546c92a946a7bd458feb264a731d4935e17e5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Wed, 7 Oct 2026 07:03:00 +0200 Subject: [PATCH 2/3] demo: vendor Leaflet, fix polyline precision re-decode, fix verify.mjs string compare Verified: WASM (emsdk 6.0.11) matches the native reference on all 19 values, and a Playwright smoke test exercised all seven panels plus mobile layout. --- demo/README.md | 4 +- demo/test/verify.mjs | 17 +- demo/web/index.html | 6 +- demo/web/vendor/leaflet/LICENSE | 26 + demo/web/vendor/leaflet/images/layers-2x.png | Bin 0 -> 1259 bytes demo/web/vendor/leaflet/images/layers.png | Bin 0 -> 696 bytes .../vendor/leaflet/images/marker-icon-2x.png | Bin 0 -> 2464 bytes .../web/vendor/leaflet/images/marker-icon.png | Bin 0 -> 1466 bytes .../vendor/leaflet/images/marker-shadow.png | Bin 0 -> 618 bytes demo/web/vendor/leaflet/leaflet.css | 661 ++++++++++++++++++ demo/web/vendor/leaflet/leaflet.js | 6 + 11 files changed, 710 insertions(+), 10 deletions(-) create mode 100644 demo/web/vendor/leaflet/LICENSE create mode 100644 demo/web/vendor/leaflet/images/layers-2x.png create mode 100644 demo/web/vendor/leaflet/images/layers.png create mode 100644 demo/web/vendor/leaflet/images/marker-icon-2x.png create mode 100644 demo/web/vendor/leaflet/images/marker-icon.png create mode 100644 demo/web/vendor/leaflet/images/marker-shadow.png create mode 100644 demo/web/vendor/leaflet/leaflet.css create mode 100644 demo/web/vendor/leaflet/leaflet.js diff --git a/demo/README.md b/demo/README.md index 9c739dd..a29de04 100644 --- a/demo/README.md +++ b/demo/README.md @@ -3,8 +3,8 @@ An interactive map page where every number is computed by the C++ library compiled to WebAssembly: distance & bearing, offset, geofence (point-in-polygon), polyline encode/decode (precision 5 and 6), Douglas–Peucker simplify, snap-to-route, and spherical area. -Client-side only: no backend, no API keys, no trackers. Map tiles come from OpenStreetMap -and Leaflet from unpkg. +Client-side only: no backend, no API keys, no trackers. Map tiles come from OpenStreetMap; +Leaflet 1.9.4 (BSD-2) is vendored in `web/vendor/leaflet/`, so the only external request is for tiles. ## Layout diff --git a/demo/test/verify.mjs b/demo/test/verify.mjs index 44eebb4..50976fd 100644 --- a/demo/test/verify.mjs +++ b/demo/test/verify.mjs @@ -41,11 +41,18 @@ const actual = { let bad = 0; for (const [key, want] of Object.entries(expected)) { const got = actual[key]; - const a = Array.isArray(want) ? want : [want]; - const b = Array.isArray(got) ? got : [got]; - const diff = typeof want === 'string' ? (want === got ? 0 : Infinity) - : Math.max(a.length === b.length ? 0 : Infinity, ...a.map((x, i) => Math.abs(x - b[i]))); - const ok = diff <= 1e-9 * Math.max(1, ...a.map(Math.abs)); + let ok, diff; + if (typeof want === 'string') { + ok = want === got; + diff = ok ? 0 : `"${want}" != "${got}"`; + } else { + const a = Array.isArray(want) ? want : [want]; + const b = Array.isArray(got) ? got : [got]; + diff = a.length === b.length ? Math.max(...a.map((x, i) => Math.abs(x - b[i]))) : Infinity; + // Native and WASM run the same C++ (libm sin/cos/asin may differ in the last ulps). + // Tolerance is relative to the magnitude, so km-scale areas are not held to nanometers. + ok = diff <= 1e-9 * Math.max(1, ...a.map(Math.abs)); + } if (!ok) bad++; console.log(`${ok ? 'OK ' : 'FAIL'} ${key.padEnd(20)} max|Δ| = ${diff}`); } diff --git a/demo/web/index.html b/demo/web/index.html index 69d9bed..bcffee4 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -5,7 +5,7 @@ geo-utils-cpp — live demo - +