A bike sensor data visualization tool that processes GPS and wheel rotation data to display bike trips with speed-colored routes on an interactive map.
This project takes raw CSV files from bike sensors and transforms them into:
- Individual trip visualizations with speed-colored segments
- Aggregated route maps showing average speeds across multiple trips
- Interactive web visualization powered by MapLibre GL JS
Reflector-Ride-Maps/
βββ csv_data/ # Raw CSV files from sensors (you create this)
βββ sensor_data/ # Cleaned GeoJSON files (generated)
βββ processed_sensor_data/ # Speed-calculated trips (generated)
βββ public/
β βββ trips.pmtiles # Compressed trip data for map
βββ csv_to_geojson_converter.py # Step 1: Convert CSVs to GeoJSON
βββ combined_processor.py # Step 2: Calculate speeds from sensor data
βββ build_pmtiles.py # Step 3: Build PMTiles for web
βββ index.html # Main visualization page
βββ app.js # Map logic and interactions
βββ config.js # Configuration (uses .env)
βββ styles.css # Styling
βββ vite.config.js # Vite bundler config
βββ package.json # Node dependencies
βββ .env # Your Mapbox token (keep secret!)
- Python 3.x for data processing
- Node.js & npm for web development
- Tippecanoe for PMTiles generation:
brew install tippecanoe # macOS - MapLibre token (free at mapbox.com)
-
Clone the repository:
git clone https://github.com/tomvanarman/Reflector-Ride-Maps.git cd Reflector-Ride-Maps -
Install Node dependencies:
npm install
-
Set up your environment:
# Create .env file echo "VITE_MAPBOX_TOKEN=pk.YOUR_TOKEN_HERE" > .env
Place your CSV files in a csv_data/ folder, then run:
python csv_to_geojson_converter.pyWhat it does:
- Reads CSV files with GPS coordinates and sensor data
- Converts to GeoJSON format with LineString geometries
- Organizes by sensor ID (e.g.,
602B3,604F0) - Extracts metadata from CSV footers
- Output:
sensor_data/{sensor_id}/{sensor_id}_Trip{N}_clean.geojson
Input CSV format:
latitude,longitude,HH:mm:ss,SSS,marker,HRot Count,Samples,Speed
52.3644,4.9130,14:23:45,123,2,100,1000,
52.3645,4.9131,14:23:46,123,3,102,1050,234
...
Bike: Trek 820
Distance: 5.2 km
python combined_processor.pyWhat it does:
- Reads cleaned GeoJSON files from
sensor_data/ - Calculates speed using wheel rotation (HRot) data:
- Uses 660mm (26") wheel diameter
- Formula:
speed = (wheel_rotations Γ circumference) / time
- Creates line segments only where the wheel actually moved
- Filters out stopped periods and anomalies
- Output:
processed_sensor_data/{sensor_id}_Trip{N}_processed.geojson
Key calculations:
- Wheel circumference: ~2.073 meters
- Sample rate: 50 Hz (0.02 seconds per sample)
- Speed cap: 50 km/h (to filter unrealistic values)
Properties added:
Speed: km/h calculated from wheel rotationhrot_diff: Wheel rotation differencetime_diff_s: Time between pointsgps_distance_m: GPS distance (for validation)
python aggregate_routes.pyWhat it does:
- Reads all processed trips
- Groups segments by road location (rounded coordinates)
- Calculates statistics per road segment:
- Average speed
- Min/max speeds
- Sample count (how many trips)
- Speed variance
- Filters out low-quality segments (stopped, large gaps)
- Output:
public/aggregated_routes.geojson
Quality filters:
- Skip speeds < 3 km/h with GPS distance > 50m
- Skip time gaps > 5 seconds
- Require minimum 2 samples per segment
python build_pmtiles.pyWhat it does:
- Uses Tippecanoe to compress processed GeoJSON into PMTiles format
- PMTiles = efficient vector tiles for web maps
- Preserves
Speed,marker, andtrip_idproperties - Output:
public/trips.pmtiles
Why PMTiles?
- Efficient: ~90% smaller than GeoJSON
- Fast: Only loads visible tiles
- Standard: Works with MapLibre/Mapbox
npx viteThe map will open at http://localhost:5173/
Individual Trips:
- β View all trips or filter by specific trip
- π¨ Color segments by speed (gradient or categories)
- π±οΈ Click segments to see speed details
Aggregated Routes:
- π See average speeds across all trips
- π’ Filter by minimum sample count (2-20 trips)
- π View speed statistics per segment
Speed Legend:
- π¦ Gray: Stopped (0-2 km/h)
- π΄ Red: Very Slow (2-5 km/h)
- π Orange: Slow (5-10 km/h)
- π‘ Yellow: Moderate (10-15 km/h)
- π’ Green: Fast (15-20 km/h)
- π’ Dark Green: Very Fast (20-25 km/h)
- π’ Darkest Green: Extreme (25-30 km/h)
- π© Super Fast (30+ km/h)
Purpose: Modern development server and build tool
Benefits:
- β‘ Lightning-fast hot module reloading (instant updates)
- π¦ Handles ES modules natively (no webpack config)
- π Injects
.envvariables into your code - ποΈ Optimized production builds
- π― Simple setup, zero configuration needed
Alternative: You could use plain HTML + file:// protocol, but:
- β No
.envfile support (token visible in code) - β No hot reloading (manual refresh needed)
- β Module imports don't work
- β No CORS handling for local files
Purpose: Open-source map rendering library
Why not Mapbox GL JS?
- β MapLibre is free and open source
- β
Has
addProtocolfor PMTiles (Mapbox v3 removed it) - β 100% API-compatible with Mapbox v2
- β No usage limits or pricing tiers
Purpose: Efficient vector tile format
Benefits:
- π¦ Single file (no tile server needed)
- π 90% smaller than GeoJSON
- β‘ Only loads visible map tiles
- π Works with static hosting (GitHub Pages, S3, etc.)
csv_to_geojson_converter.py: Converts raw sensor CSVs to GeoJSON LineStringscombined_processor.py: Calculates speeds from wheel rotation data (HRot)aggregate_routes.py: Aggregates speeds across trips to show typical speeds per roadbuild_pmtiles.py: Compresses GeoJSON into PMTiles using Tippecanoe
index.html: Main webpage with map container and controlsapp.js: Map initialization, data loading, speed coloring, click handlersconfig.js: Configuration (Mapbox token, file paths, map style)styles.css: UI styling for controls, legend, and stats panelvite.config.js: Vite configuration for loading .env variables
.env: Your Mapbox token (DO NOT commit to Git!).gitignore: Prevents committing sensitive files and generated datapackage.json: Node.js dependencies and scriptspackage-lock.json: Locked dependency versions
generate-config.js: Alternative script to generate config from .env (not currently used)sensor_data/: Cleaned GeoJSON from CSV conversionprocessed_sensor_data/: Speed-calculated tripspublic/trips.pmtiles: Compressed trip datapublic/aggregated_routes.geojson: Aggregated speed routes
WHEEL_DIAMETER_MM = 660 # 26 inch wheel
WHEEL_CIRCUMFERENCE_M = (660 / 1000) * math.pi # ~2.073mMAP_CENTER: [4.9, 52.37], // Amsterdam area [lon, lat]
MAP_ZOOM: 11, // Initial zoom level
MAP_STYLE: 'https://...' // CartoDB Dark Matter styleModify the getSpeedColorExpression() function to adjust color thresholds.
- Check that
Speedproperty exists in your processed GeoJSON - Verify wheel diameter is correct for your bike
- Run:
python build_pmtiles.pyto rebuild with-y Speedflag
- Check browser console for errors
- Verify
.envhas correct token:VITE_MAPBOX_TOKEN=pk.ey... - Restart Vite:
Ctrl+Cthennpx vite
- Ensure
public/trips.pmtilesexists - Check file paths in
config.js - Verify trips have coordinates in Amsterdam area
- Make sure scripts have
type="module"inindex.html - Check that Vite is running (not just opening HTML file)
- Fork the repository
- Create a feature branch:
git checkout -b feature-name - Commit changes:
git commit -am 'Add feature' - Push to branch:
git push origin feature-name - Submit a Pull Request
ISC License - See package.json for details
- MapLibre GL JS: Open-source mapping library
- Tippecanoe: PMTiles generation by Mapbox
- CartoDB: Free basemap styles
- Vite: Modern build tooling
For questions or issues, please open a GitHub issue or contact the maintainers.
Happy mapping! π΄ββοΈπΊοΈ