diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml new file mode 100644 index 0000000..e189889 --- /dev/null +++ b/.github/workflows/draft-pdf.yml @@ -0,0 +1,28 @@ +name: Draft PDF +on: + push: + paths: + - paper/** + - .github/workflows/draft-pdf.yml + +jobs: + paper: + runs-on: ubuntu-latest + name: Paper Draft + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build draft PDF + uses: openjournals/openjournals-draft-action@master + with: + journal: joss + # This should be the path to the paper within your repo. + paper-path: paper/paper.md + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: paper + # This is the output path where Pandoc will write the compiled + # PDF. Note, this should be the same directory as the input + # paper.md + path: paper/paper.pdf \ No newline at end of file diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 9f0c923..03396ce 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -62,26 +62,26 @@ jobs: cd dist/python maturin build --release # Publish Rust package to crates.io - publish-rust: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Sync packages - run: ./scripts/sync_packages.sh - - - name: Publish to crates.io - run: | - cd dist/rust - cargo publish --allow-dirty - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + #publish-rust: + # needs: build + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # + # - name: Setup Rust + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: stable + # + # - name: Sync packages + # run: ./scripts/sync_packages.sh + # + # - name: Publish to crates.io + # run: | + # cd dist/rust + # cargo publish --allow-dirty + # env: + # CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} # Publish JavaScript package to NPM and GitHub Packages publish-javascript: @@ -148,33 +148,33 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Publish Python package to PyPI - publish-python: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install maturin build twine - - - name: Sync packages - run: ./scripts/sync_packages.sh - - - name: Build and publish Python package - run: | - cd dist/python - maturin publish - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file + #publish-python: + # needs: build + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # + # - name: Setup Rust + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: stable + # + # - name: Setup Python + # uses: actions/setup-python@v4 + # with: + # python-version: '3.8' + # + # - name: Install Python dependencies + # run: | + # python -m pip install --upgrade pip + # pip install maturin build twine + # + # - name: Sync packages + # run: ./scripts/sync_packages.sh + # + # - name: Build and publish Python package + # run: | + # cd dist/python + # maturin publish + # env: + # MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 517588d..6028cc4 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,284 @@ -# fastgeotoolkit [![Crates.io](https://img.shields.io/crates/v/fastgeotoolkit)](https://crates.io/crates/fastgeotoolkit) [![PyPI](https://img.shields.io/pypi/v/fastgeotoolkit)](https://pypi.org/project/fastgeotoolkit/) [![npm](https://img.shields.io/npm/v/fastgeotoolkit)](https://www.npmjs.com/package/fastgeotoolkit) -[![codecov](https://codecov.io/gh/a0a7/fastgeotoolkit/branch/main/graph/badge.svg)](https://codecov.io/gh/a0a7/fastgeotoolkit) +# fastgeotoolkit [![npm](https://img.shields.io/npm/v/fastgeotoolkit)](https://www.npmjs.com/package/fastgeotoolkit) [![Docs](https://img.shields.io/badge/Documentation-skyblue?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNjQwIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDcuMC4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjUgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTQ4MCA1NzZMMTkyIDU3NkMxMzkgNTc2IDk2IDUzMyA5NiA0ODBMOTYgMTYwQzk2IDEwNyAxMzkgNjQgMTkyIDY0TDQ5NiA2NEM1MjIuNSA2NCA1NDQgODUuNSA1NDQgMTEyTDU0NCA0MDBDNTQ0IDQyMC45IDUzMC42IDQzOC43IDUxMiA0NDUuM0w1MTIgNTEyQzUyOS43IDUxMiA1NDQgNTI2LjMgNTQ0IDU0NEM1NDQgNTYxLjcgNTI5LjcgNTc2IDUxMiA1NzZMNDgwIDU3NnpNMTkyIDQ0OEMxNzQuMyA0NDggMTYwIDQ2Mi4zIDE2MCA0ODBDMTYwIDQ5Ny43IDE3NC4zIDUxMiAxOTIgNTEyTDQ0OCA1MTJMNDQ4IDQ0OEwxOTIgNDQ4ek0yMjQgMjE2QzIyNCAyMjkuMyAyMzQuNyAyNDAgMjQ4IDI0MEw0MjQgMjQwQzQzNy4zIDI0MCA0NDggMjI5LjMgNDQ4IDIxNkM0NDggMjAyLjcgNDM3LjMgMTkyIDQyNCAxOTJMMjQ4IDE5MkMyMzQuNyAxOTIgMjI0IDIwMi43IDIyNCAyMTZ6TTI0OCAyODhDMjM0LjcgMjg4IDIyNCAyOTguNyAyMjQgMzEyQzIyNCAzMjUuMyAyMzQuNyAzMzYgMjQ4IDMzNkw0MjQgMzM2QzQzNy4zIDMzNiA0NDggMzI1LjMgNDQ4IDMxMkM0NDggMjk4LjcgNDM3LjMgMjg4IDQyNCAyODhMMjQ4IDI4OHoiLz48L3N2Zz4=)](https://fastgeotoolkit.pages.dev/) [![Demo](https://img.shields.io/badge/Try%20the%20demo-lightblue)](https://fastgeotoolkit-demo.pages.dev/) + + + [![Rust Tests](https://github.com/a0a7/fastgeotoolkit/actions/workflows/rust-tests.yml/badge.svg)](https://github.com/a0a7/fastgeotoolkit/actions/workflows/rust-tests.yml) [![JavaScript Tests](https://github.com/a0a7/fastgeotoolkit/actions/workflows/javascript-tests.yml/badge.svg)](https://github.com/a0a7/fastgeotoolkit/actions/workflows/javascript-tests.yml) [![CodeQL](https://github.com/a0a7/fastgeotoolkit/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/a0a7/fastgeotoolkit/actions/workflows/github-code-scanning/codeql) ![License](https://img.shields.io/badge/license-MIT-blue) -GPS track processor for frequency-based route heatmaps from GPX, FIT, and polyline data. +fastgeotoolkit is a library for GPS data processing and route density mapping. The core of the library is written in Rust and it's compiled to webassembly for use in the browser and node. + +> [!NOTE] +> Only Javascript/Typescript is supported at the moment. Rust and Python releases are planned. + +## What it does + +The main use case is creating route heatmaps where you want to see which paths/routes are used most frequently. You can test this functionality at [https://fastgeotoolkit-demo.pages.dev/](https://fastgeotoolkit-demo.pages.dev/), using either your own data or sample data. This is an example of what a heatmap produced using fastgeotoolkit looks like: +![https://i.ibb.co/MxpHbVdp/image.png](https://i.ibb.co/MxpHbVdp/image.png) + +However, beyond this primary usecase, this library helps you: +- Analyze GPS tracks (distance, statistics, intersections) +- Decode Google polylines +- Convert between GPS data formats + +## Documentation + +Docs are available at https://fastgeotoolkit.pages.dev/. ## Installation -| Language | Package Manager | Command | -|----------|----------------|---------| -| **Rust** | cargo | `cargo add fastgeotoolkit` | -| **Python** | pip | `pip install fastgeotoolkit` | -| **JavaScript/Node.js** | npm | `npm install fastgeotoolkit` | -| **JavaScript/Node.js** | GitHub Packages | `npm install @a0a7/fastgeotoolkit` | +```bash +npm install fastgeotoolkit +# or +pnpm i fastgeotoolkit +``` -> **Note**: The JavaScript package is available on both npm and GitHub Packages with the same functionality. +## Basic Usage -## Features -- GPX file parsing -- FIT file parsing -- Polyline decoding -- Route frequency analysis -- WebAssembly bindings +```typescript +import { processGpxFiles } from 'fastgeotoolkit'; -## Docs +// Process GPX files into a heatmap +const gpxFile1 = new Uint8Array(/* your GPX file data */); +const gpxFile2 = new Uint8Array(/* another GPX file */); -**[full docs](https://a0a7.github.io/fastgeotoolkit/)** +const result = await processGpxFiles([gpxFile1, gpxFile2]); -- [Rust API](https://docs.rs/fastgeotoolkit) -- [JS/TS API](https://a0a7.github.io/fastgeotoolkit/api/typescript) -- [Python API](https://a0a7.github.io/fastgeotoolkit/api/python) +// Result contains tracks with frequency data +console.log(`Found ${result.tracks.length} unique track segments`); +console.log(`Maximum frequency: ${result.max_frequency}`); -## Installation +result.tracks.forEach(track => { + console.log(`Track with ${track.coordinates.length} points, used ${track.frequency} times`); +}); +``` -### JavaScript/TypeScript +## Working with Polylines -```bash -# Install from npm (latest version) -npm install fastgeotoolkit +```typescript +import { decodePolyline, processPolylines } from 'fastgeotoolkit'; + +// Decode a single polyline +const coords = await decodePolyline('_p~iF~ps|U_ulLnnqC_mqNvxq`@'); +console.log(coords); // [[lat, lng], [lat, lng], ...] -# Or install from GitHub Packages -npm install @a0a7/fastgeotoolkit --registry=https://npm.pkg.github.com +// Process multiple polylines into a heatmap +const polylines = [ + '_p~iF~ps|U_ulLnnqC_mqNvxq`@', + 'another_encoded_polyline', + 'yet_another_one' +]; +const heatmap = await processPolylines(polylines); ``` -### Rust +## Track Analysis + +```typescript +import { calculateTrackStatistics, validateCoordinates } from 'fastgeotoolkit'; + +const coordinates = [[37.7749, -122.4194], [37.7849, -122.4094]]; // [lat, lng] pairs -```toml -# Add to Cargo.toml -[dependencies] -fastgeotoolkit = "0.1.0" +// Get basic statistics +const stats = await calculateTrackStatistics(coordinates); +console.log(`Distance: ${stats.distance_km.toFixed(2)} km`); +console.log(`${stats.point_count} GPS points`); +console.log(`Bounds: ${stats.bounding_box}`); // [min_lat, min_lng, max_lat, max_lng] + +// Validate coordinates +const validation = await validateCoordinates(coordinates); +console.log(`${validation.valid_count} out of ${validation.total_count} coordinates are valid`); +if (validation.issues.length > 0) { + console.log('Issues found:', validation.issues); +} ``` -### Python +## Data Conversion -```bash -pip install fastgeotoolkit +```typescript +import { coordinatesToGeojson, exportToGpx } from 'fastgeotoolkit'; + +// Convert to GeoJSON +const geojson = await coordinatesToGeojson(coordinates, { + name: 'My Route', + activity: 'cycling' +}); + +// Export multiple tracks as GPX +const tracks = [track1_coordinates, track2_coordinates]; +const gpxString = await exportToGpx(tracks, { + creator: 'My App', + name: 'Route Collection' +}); +``` + +## Real-world Example + +Here's an example of how you might use this in a web app to show route popularity: + +```typescript +import { processGpxFiles } from 'fastgeotoolkit'; + +async function createHeatmap(gpxFiles) { + // Convert files to Uint8Array + const fileBuffers = await Promise.all( + gpxFiles.map(file => file.arrayBuffer().then(buf => new Uint8Array(buf))) + ); + + // Process into heatmap + const heatmap = await processGpxFiles(fileBuffers); + + // Render on map (example with any mapping library) + heatmap.tracks.forEach(track => { + const intensity = track.frequency / heatmap.max_frequency; + const color = `hsl(${(1-intensity) * 240}, 100%, 50%)`; // blue to red + + drawLineOnMap(track.coordinates, { + color: color, + weight: Math.max(2, intensity * 8) + }); + }); +} + +// Usage +document.getElementById('file-input').addEventListener('change', async (e) => { + const files = Array.from(e.target.files); + await createHeatmap(files); +}); ``` -## Example Usage +## TypeScript Support + +The library includes full TypeScript definitions: + +```typescript +import type { + Coordinate, // [number, number] - [lat, lng] + HeatmapResult, // { tracks: HeatmapTrack[], max_frequency: number } + HeatmapTrack, // { coordinates: Coordinate[], frequency: number } + TrackStatistics, // distance, bounds, point count, etc. + ValidationResult, // validation results with issues + FileInfo // file format information +} from 'fastgeotoolkit'; +``` -### JavaScript/TypeScript +## JavaScript Utilities + +For simple operations that don't rely on WebAssembly: + +```typescript +import { utils } from 'fastgeotoolkit'; + +// Basic coordinate validation +if (utils.isValidCoordinate(37.7749, -122.4194)) { + console.log('Valid GPS coordinate'); +} + +// Calculate distance between two points +const distance = utils.haversineDistance(37.7749, -122.4194, 37.7849, -122.4094); +console.log(`Distance: ${distance.toFixed(2)} km`); + +// Get bounding box +const bounds = utils.getBoundingBox(coordinates); +console.log(`Bounds: ${bounds}`); // [min_lat, min_lng, max_lat, max_lng] +``` + +## Browser vs Node.js + +Works the same in both environments: ```javascript -import { processGpxFiles, decodePolyline } from 'fastgeotoolkit'; +// Browser +import { processGpxFiles } from 'fastgeotoolkit'; + +// Node.js +const { processGpxFiles } = require('fastgeotoolkit'); +// or with ES modules: +import { processGpxFiles } from 'fastgeotoolkit'; +``` + +## Performance Notes + +- WebAssembly provides near-native performance for GPS processing +- Large datasets (thousands of tracks) process quickly +- First function call initializes WebAssembly (adds ~100ms startup time) + +## Common Issues -// Process GPX files -const gpxFiles = [/* ArrayBuffer[] */]; -const result = await processGpxFiles(gpxFiles); +**"Cannot resolve module"** errors: Make sure your bundler supports WebAssembly. Modern bundlers (Vite, Webpack 5+, etc.) work out of the box. -// Decode polyline -const decoded = decodePolyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); +**TypeScript errors**: Ensure you're using TypeScript 4.0+ for proper WebAssembly typing support. + +**File reading**: Remember to convert File objects to Uint8Array: +```javascript +const buffer = await file.arrayBuffer(); +const uint8Array = new Uint8Array(buffer); ``` -### Rust +## Development & Maintenance + +This project consists of Rust code compiled to WebAssembly with JavaScript/TypeScript bindings. + +### Project Structure -```rust -use heatmap_parse::{process_gpx_files, decode_polyline, process_polylines}; +- `/src/` - Rust source code +- `/dist/javascript/` - JavaScript/TypeScript bindings and NPM package +- `/dist/wasm/` - Generated WebAssembly files +- `/demo/` - Demo application (SvelteKit) +- `/docs/` - Generated documentation +> [!NOTE] +> `/dist/python` and `/dist/rust/` contain WIP releases for their respective ecosystems, but they're not in working order yet. -let files = vec![/* Vec file data */]; -let result = process_gpx_files(files); +### Compiling Rust to WebAssembly -let coords = decode_polyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); +To compile the Rust code to WebAssembly: -let polylines = vec!["polyline1".to_string(), "polyline2".to_string()]; -let tracks = process_polylines(polylines); +```bash +# Install wasm-pack if you haven't already +cargo install wasm-pack + +# Build the WebAssembly module +wasm-pack build --target web --out-dir dist/wasm ``` -### WebAssembly +### Building the NPM Package -```javascript -import init, { process_gpx_files, decode_polyline_string } from 'fastgeotoolkit'; +To build the complete NPM package with all bindings: -await init(); +```bash +# From the root directory +npm run build + +# Or build individual components: +npm run build:wasm # Build WebAssembly +npm run build:js # Build JavaScript bindings +npm run build:docs # Build documentation +``` -const files = [/* Uint8Array buffers */]; -const result = process_gpx_files(files); +### Building Documentation -const coords = decode_polyline_string("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); +The documentation is generated using TypeDoc and can be built locally: + +```bash +cd dist/javascript +npm run docs ``` -## Building +This will generate the documentation website in the `docs/` directory. + +### Testing ```bash -# Native Rust -cargo build --release +# Run Rust tests +cargo test -# js -wasm-pack build --target web +# Run JavaScript tests +cd dist/javascript +npm test ``` ## License -MIT \ No newline at end of file +MIT diff --git a/codemeta.json b/codemeta.json deleted file mode 100644 index 1661573..0000000 --- a/codemeta.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "@context": "https://doi.org/10.5063/schema/codemeta-2.0", - "type": "SoftwareSourceCode", - "applicationCategory": "GIS", - "codeRepository": "https://github.com/a0a7/fastgeotoolkit.git", - "contributor": [ - { - "id": "https://orcid.org/0009-0008-5679-3042", - "type": "Person", - "affiliation": { - "type": "Organization", - "name": "B.E.E. Student, University of Minnesota" - }, - "email": "aw@a0.ax", - "familyName": "Weimer", - "givenName": "Alexander" - }, - { - "type": "schema:Role", - "contributor": "https://orcid.org/0009-0008-5679-3042", - "schema:endDate": "9999-12-31", - "schema:roleName": "Developer", - "schema:startDate": "2025-04-11" - }, - { - "id": "_:contributor_2", - "type": "Person", - "affiliation": { - "type": "Organization", - "name": "Student, University of Minnesota" - }, - "email": "abrah844@umn.edu", - "familyName": "Abraham", - "givenName": "Justin" - } - ], - "dateCreated": "2025-04-11", - "dateModified": "2025-07-30", - "description": "High-Performance Geospatial Analysis library with Novel Segment-Based Route Density Mapping", - "keywords": [ - "Geospatial computing", - "heatmap visualization", - "Rust", - "WebAssembly" - ], - "license": "https://spdx.org/licenses/MIT", - "name": "fastGeoToolkit", - "programmingLanguage": "Rust", - "contIntegration": "https://github.com/a0a7/fastgeotoolkit/actions", - "codemeta:continuousIntegration": { - "id": "https://github.com/a0a7/fastgeotoolkit/actions" - }, - "issueTracker": "https://github.com/a0a7/fastgeotoolkit/issues" -} \ No newline at end of file diff --git a/core/Cargo.lock b/core/Cargo.lock index 724f1bd..ec57685 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -31,9 +31,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -267,15 +267,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", diff --git a/core/Cargo.toml b/core/Cargo.toml index 0dfe83c..ad4ce7e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" description = "A fast and comprehensive geospatial toolkit for processing GPS tracks, trajectories, and coordinate data from GPX, FIT, and polyline formats" license = "MIT" repository = "https://github.com/a0a7/fastgeotoolkit" -authors = ["fastgeotoolkit contributors"] +authors = ["a0a7"] keywords = ["gps", "geospatial", "trajectory", "fast", "toolkit"] categories = ["wasm", "science", "visualization"] readme = "README.md" diff --git a/core/README.md b/core/README.md index 5950942..ea53090 100644 --- a/core/README.md +++ b/core/README.md @@ -168,7 +168,7 @@ If you use fastGeoToolkit in academic research, please cite: ```bibtex @software{fastgeotoolkit2024, title={fastGeoToolkit: A Novel High-Performance Geospatial Analysis Framework with Advanced Route Density Mapping}, - author={fastGeoToolkit Contributors}, + author={a0a7}, year={2024}, url={https://github.com/a0a7/fastgeotoolkit}, version={0.1.3} diff --git a/demo/_headers b/demo/_headers new file mode 100644 index 0000000..d4383bd --- /dev/null +++ b/demo/_headers @@ -0,0 +1,7 @@ +/_app/* + Cache-Control: public, max-age=31536000, immutable + +/*.wasm + Content-Type: application/wasm + Cache-Control: public, max-age=31536000 + Cross-Origin-Resource-Policy: cross-origin diff --git a/demo/package.json b/demo/package.json index 7081435..dc38b33 100644 --- a/demo/package.json +++ b/demo/package.json @@ -53,7 +53,7 @@ "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/turf": "^7.2.0", - "fastgeotoolkit": "^0.2.0", + "fastgeotoolkit": "^0.2.3", "maplibre-gl": "^5.6.0", "svelte-loading-spinners": "^0.3.6", "svelte-maplibre": "^1.2.0" diff --git a/demo/pnpm-lock.yaml b/demo/pnpm-lock.yaml index ad5599a..fc7b971 100644 --- a/demo/pnpm-lock.yaml +++ b/demo/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^7.2.0 version: 7.2.0 fastgeotoolkit: - specifier: ^0.1.14 - version: 0.1.14 + specifier: ^0.2.3 + version: 0.2.3 maplibre-gl: specifier: ^5.6.0 version: 5.6.0 @@ -1689,8 +1689,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fastgeotoolkit@0.1.14: - resolution: {integrity: sha512-a452x1vHNFYXIJsBGtXLw6diY/BIFHaUIsnIWC2CbQxGsH7ChesQJTduUY1/ywanb2oj1gp8B2OW147gWerNLw==} + fastgeotoolkit@0.2.3: + resolution: {integrity: sha512-J3UmoHoxfPRnUlPxcFwXxuFknOTu0mLQv88ACclorcaz3D6zDveid0dPB+RB4VVfAj7IWrimK5Q0FfWIO36shQ==} engines: {node: '>=16.0.0'} fdir@6.4.6: @@ -4665,7 +4665,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fastgeotoolkit@0.1.14: {} + fastgeotoolkit@0.2.3: {} fdir@6.4.6(picomatch@4.0.2): optionalDependencies: diff --git a/demo/src/app.html b/demo/src/app.html index 77a5ff5..6986738 100644 --- a/demo/src/app.html +++ b/demo/src/app.html @@ -2,7 +2,7 @@ - + %sveltekit.head% diff --git a/demo/src/lib/ridewithgps.ts b/demo/src/lib/ridewithgps.ts index 1043086..e7b778c 100644 --- a/demo/src/lib/ridewithgps.ts +++ b/demo/src/lib/ridewithgps.ts @@ -138,7 +138,8 @@ export class RideWithGPSService { const data = await response.json(); const trips: RWGPSTrip[] = data.results || []; - if (trips.length === 0) { + // Handle case where API returns null/undefined instead of empty array + if (!trips || !Array.isArray(trips) || trips.length === 0) { break; // No more trips } @@ -217,6 +218,12 @@ export class RideWithGPSService { async getPolylinesFromTrips(trips: RWGPSTrip[], onProgress?: (processed: number, total: number) => void): Promise { const polylines: string[] = []; + // Safety check for null/undefined trips array + if (!trips || !Array.isArray(trips)) { + console.warn('getPolylinesFromTrips: trips is not a valid array'); + return polylines; + } + console.log('Sample trip data structure:', trips.length > 0 ? trips[0] : 'No trips'); for (let i = 0; i < trips.length; i++) { diff --git a/demo/src/lib/strava.ts b/demo/src/lib/strava.ts index 2d54717..82b71bd 100644 --- a/demo/src/lib/strava.ts +++ b/demo/src/lib/strava.ts @@ -136,7 +136,8 @@ export class StravaService { const activities: StravaActivity[] = await response.json(); - if (activities.length === 0) { + // Handle case where API returns null/undefined instead of empty array + if (!activities || !Array.isArray(activities) || activities.length === 0) { break; // No more activities } @@ -197,6 +198,12 @@ export class StravaService { getPolylinesFromActivities(activities: StravaActivity[]): string[] { const polylines: string[] = []; + // Safety check for null/undefined activities array + if (!activities || !Array.isArray(activities)) { + console.warn('getPolylinesFromActivities: activities is not a valid array'); + return polylines; + } + for (const activity of activities) { // Prefer detailed polyline over summary polyline const polyline = activity.map?.polyline || activity.map?.summary_polyline; diff --git a/demo/src/routes/+page.svelte b/demo/src/routes/+page.svelte index 808292d..5476cd6 100644 --- a/demo/src/routes/+page.svelte +++ b/demo/src/routes/+page.svelte @@ -31,7 +31,7 @@ onAdd(map: null) { this._map = map; this._container = document.createElement('div'); - this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group ruler-control'; + this._container.className = 'maplibregl-ctfrl maplibregl-ctrl-group ruler-control'; this._button = document.createElement('button'); this._button.className = 'ruler-control-button'; @@ -462,7 +462,11 @@ const result = await processGpxFiles(jsArray); console.log('Sample data processing result:', result); - if (result && result.tracks && result.tracks.length > 0) { + if (!result) { + throw new Error('WASM function returned null/undefined result'); + } + + if (result && result.tracks && Array.isArray(result.tracks) && result.tracks.length > 0) { const heatmapResult = result as { tracks: any[], max_frequency: number }; // Use the shared rendering function @@ -570,6 +574,10 @@ if (cached && cacheAge < cacheExpiry) { activities = JSON.parse(cached); + // Safety check for parsed data + if (!activities || !Array.isArray(activities)) { + activities = []; + } stravaActivityCount = activities.length; stravaImportProgress = 50; console.log(`Using cached ${activities.length} activities from localStorage`); @@ -577,23 +585,32 @@ } // Fetch fresh data if no valid cache - if (activities.length === 0) { + if (!activities || activities.length === 0) { activities = await stravaService.fetchAllActivities((count: number) => { stravaActivityCount = count; stravaImportProgress = Math.min(50, count); // First 50% for fetching }); - // Cache the activities in localStorage + // Safety check for fetched data + if (!activities || !Array.isArray(activities)) { + activities = []; + } + + // Cache the activities in localStorage with safe storage if (browser && activities.length > 0) { - localStorage.setItem('strava_activities', JSON.stringify(activities)); - localStorage.setItem('strava_activities_timestamp', Date.now().toString()); - console.log(`Cached ${activities.length} activities to localStorage`); + const success = safeLocalStorageSetItem('strava_activities', JSON.stringify(activities)); + if (success) { + safeLocalStorageSetItem('strava_activities_timestamp', Date.now().toString()); + console.log(`Cached ${activities.length} activities to localStorage`); + } else { + console.warn('Failed to cache activities - localStorage quota exceeded or unavailable'); + } } } console.log(`Processing ${activities.length} activities from Strava`); - if (activities.length === 0) { + if (!activities || activities.length === 0) { error = 'No activities found in your Strava account'; return; } @@ -601,6 +618,13 @@ // Extract polylines from activities const polylines = stravaService.getPolylinesFromActivities(activities); + console.log('DEBUG: Extracted polylines:', { + count: polylines.length, + firstPolyline: polylines[0]?.substring(0, 100) + '...', + polylineTypes: polylines.slice(0, 3).map(p => typeof p), + polyllineLengths: polylines.slice(0, 3).map(p => p?.length) + }); + if (polylines.length === 0) { error = 'No GPS data found in your Strava activities'; return; @@ -618,9 +642,42 @@ jsArray.push(polyline); } - const result = processPolylines(jsArray); + console.log('DEBUG: About to call processPolylines with:', { + arrayLength: jsArray.length, + firstPolylineSample: jsArray[0]?.substring(0, 50) + '...', + arrayType: Array.isArray(jsArray), + functionType: typeof processPolylines + }); + + const result = await processPolylines(jsArray); + + console.log('DEBUG: processPolylines returned:', { + result: result, + resultType: typeof result, + isNull: result === null, + isUndefined: result === undefined, + resultKeys: result ? Object.keys(result) : 'N/A', + stringified: JSON.stringify(result)?.substring(0, 200) + '...' + }); + + if (!result) { + throw new Error('WASM function returned null/undefined result'); + } + const heatmapResult = result as { tracks: any[], max_frequency: number }; + console.log('DEBUG: heatmapResult after casting:', { + hasTracks: 'tracks' in heatmapResult, + tracksType: typeof heatmapResult.tracks, + tracksIsArray: Array.isArray(heatmapResult.tracks), + tracksLength: heatmapResult.tracks?.length, + maxFrequency: heatmapResult.max_frequency + }); + + if (!heatmapResult.tracks || !Array.isArray(heatmapResult.tracks)) { + throw new Error(`WASM function returned invalid tracks data. Got: ${JSON.stringify(heatmapResult)}`); + } + console.log('WASM returned', heatmapResult.tracks.length, 'tracks with max frequency:', heatmapResult.max_frequency); stravaImportProgress = 90; @@ -632,7 +689,13 @@ console.log('Strava import completed successfully'); } catch (err) { - error = `Error importing Strava activities: ${err}`; + // Handle localStorage quota errors more gracefully + if (err instanceof DOMException && err.name === 'QuotaExceededError') { + console.warn('localStorage quota exceeded during Strava import'); + error = 'Import completed but could not cache data (storage full). Try clearing browser storage.'; + } else { + error = `Error importing Strava activities: ${err}`; + } console.error(err); } finally { isImportingStrava = false; @@ -680,6 +743,10 @@ if (cached && cacheAge < cacheExpiry) { trips = JSON.parse(cached); + // Safety check for parsed data + if (!trips || !Array.isArray(trips)) { + trips = []; + } rwgpsTripCount = trips.length; rwgpsImportProgress = 50; console.log(`Using cached ${trips.length} trips from localStorage`); @@ -693,11 +760,15 @@ rwgpsImportProgress = Math.min(50, count); // First 50% for fetching }); - // Cache the trips in localStorage + // Cache the trips in localStorage with safe storage if (browser && trips.length > 0) { - localStorage.setItem('rwgps_trips', JSON.stringify(trips)); - localStorage.setItem('rwgps_trips_timestamp', Date.now().toString()); - console.log(`Cached ${trips.length} trips to localStorage`); + const success = safeLocalStorageSetItem('rwgps_trips', JSON.stringify(trips)); + if (success) { + safeLocalStorageSetItem('rwgps_trips_timestamp', Date.now().toString()); + console.log(`Cached ${trips.length} trips to localStorage`); + } else { + console.warn('Failed to cache trips - localStorage quota exceeded or unavailable'); + } } } @@ -735,9 +806,18 @@ jsArray.push(polyline); } - const result = processPolylines(jsArray); + const result = await processPolylines(jsArray); + + if (!result) { + throw new Error('WASM function returned null/undefined result'); + } + const heatmapResult = result as { tracks: any[], max_frequency: number }; + if (!heatmapResult.tracks || !Array.isArray(heatmapResult.tracks)) { + throw new Error('WASM function returned invalid tracks data'); + } + console.log('WASM returned', heatmapResult.tracks.length, 'tracks with max frequency:', heatmapResult.max_frequency); rwgpsImportProgress = 90; @@ -749,7 +829,13 @@ console.log('RideWithGPS import completed successfully'); } catch (err) { - error = `Error importing RideWithGPS trips: ${err}`; + // Handle localStorage quota errors more gracefully + if (err instanceof DOMException && err.name === 'QuotaExceededError') { + console.warn('localStorage quota exceeded during RideWithGPS import'); + error = 'Import completed but could not cache data (storage full). Try clearing browser storage.'; + } else { + error = `Error importing RideWithGPS trips: ${err}`; + } console.error(err); } finally { isImportingRWGPS = false; @@ -891,7 +977,7 @@ } }); - console.log('Strava-style multi-layer heatmap added successfully'); + console.log('heatmap added successfully'); // Fit map to show all tracks if (heatmapResult.tracks.length > 0) { @@ -1424,17 +1510,58 @@ // Initialize WASM module dynamically in browser only if (browser) { try { + console.log('Attempting to load fastgeotoolkit...'); + // Import the published fastgeotoolkit npm package const fastGeoToolkit = await import('fastgeotoolkit'); + console.log('fastgeotoolkit module loaded:', fastGeoToolkit); - // Just assign the functions - they will automatically handle WASM loading + // Use the package's built-in WASM initialization if available + if (typeof fastGeoToolkit.initWithWasm === 'function') { + console.log('Using built-in WASM initialization...'); + await fastGeoToolkit.initWithWasm(); + } else if (typeof fastGeoToolkit.ensureWasmInitialized === 'function') { + console.log('Using ensureWasmInitialized...'); + await fastGeoToolkit.ensureWasmInitialized(); + } + + // Assign the functions - they should now work with WASM loaded processGpxFiles = fastGeoToolkit.processGpxFiles; processPolylines = fastGeoToolkit.processPolylines; console.log('WASM initialized successfully from fastgeotoolkit npm package'); } catch (err) { console.error('Failed to load fastgeotoolkit npm package:', err); - error = `Failed to load fastgeotoolkit module: ${err}`; + console.error('Full error details:', err); + + // If the automatic WASM loading fails, try to load from a CDN as fallback + try { + console.log('Trying CDN fallback...'); + const cdnModule = await import('https://unpkg.com/fastgeotoolkit@latest/dist/index.esm.js'); + + if (typeof cdnModule.initWithWasm === 'function') { + await cdnModule.initWithWasm(); + } + + processGpxFiles = cdnModule.processGpxFiles; + processPolylines = cdnModule.processPolylines; + + console.log('Successfully loaded from CDN fallback'); + } catch (cdnErr) { + console.error('CDN fallback also failed:', cdnErr); + error = `Failed to load fastgeotoolkit module: ${err}. CDN fallback also failed: ${cdnErr}`; + + // Provide more specific error information + if (err instanceof Error) { + if (err.message.includes('WASM')) { + error = 'WASM module failed to load. This may be due to Content Security Policy or bundling issues.'; + } else if (err.message.includes('Failed to fetch')) { + error = 'Network error loading the module. Please check your internet connection.'; + } else if (err.message.includes('Failed to resolve module specifier')) { + error = 'Module resolution error. The fastgeotoolkit package may not be properly bundled for this environment.'; + } + } + } } } @@ -1627,6 +1754,66 @@ } } } + + // Safe localStorage operations + function safeLocalStorageSetItem(key: string, value: string): boolean { + if (!browser || typeof localStorage === 'undefined') { + return false; + } + + try { + // Clear potentially large items before setting new data + const keysToCheck = ['strava_activities', 'rwgps_trips']; + if (keysToCheck.includes(key)) { + // Clear old data first to free up space + keysToCheck.forEach(k => { + if (k !== key) { + localStorage.removeItem(k); + localStorage.removeItem(k + '_timestamp'); + } + }); + } + + localStorage.setItem(key, value); + return true; + } catch (error) { + if (error instanceof DOMException && error.name === 'QuotaExceededError') { + console.warn('localStorage quota exceeded, clearing cache and retrying...'); + // Clear all cached data and try again + try { + localStorage.removeItem('strava_activities'); + localStorage.removeItem('strava_activities_timestamp'); + localStorage.removeItem('rwgps_trips'); + localStorage.removeItem('rwgps_trips_timestamp'); + localStorage.setItem(key, value); + return true; + } catch (retryError) { + console.warn('localStorage still failed after clearing cache:', retryError); + return false; + } + } else { + console.warn('localStorage error:', error); + return false; + } + } + } + + // Clear all cached data + function clearLocalStorageCache() { + if (!browser || typeof localStorage === 'undefined') { + return; + } + + try { + localStorage.removeItem('strava_activities'); + localStorage.removeItem('strava_activities_timestamp'); + localStorage.removeItem('rwgps_trips'); + localStorage.removeItem('rwgps_trips_timestamp'); + console.log('Cleared all cached activity/trip data'); + } catch (error) { + console.warn('Error clearing localStorage cache:', error); + } + }