Skip to content

Commit 1141ce8

Browse files
author
Alfiya Tarasenko
committed
Add Map Matching API example and demo
1 parent 8d7d044 commit 1141ce8

14 files changed

Lines changed: 95358 additions & 0 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Welcome to the **Geoapify Location Platform Code Samples** repository! This proj
1212
* [Visualize Isochrones with Leaflet](#javascript-visualize-isochrones-with-leaflet)
1313
* [Visualize Isochrones with MapLibre GL](#javascript-visualize-isochrones-with-maplibre-gl)
1414
* [Route Elevation Profile with MapLibre](#javascript-route-elevation-profile-with-maplibre)
15+
* [GPX Map Matching (Snap to Roads) with Geoapify & MapLibre GL](#javascript-gpx-map-matching-snap-to-roads-with-geoapify--maplibre-gl)
1516

1617
### Node.js
1718

@@ -136,6 +137,36 @@ Users define start and end locations by clicking on the map or typing addresses.
136137

137138
---
138139

140+
### JavaScript: [GPX Map Matching (Snap to Roads) with Geoapify & MapLibre GL](https://github.com/geoapify/maps-api-code-samples/tree/main/javascript/map-matching-to-snap-gpx-to-roads-maplibre)
141+
142+
**What it does:**
143+
Takes GPS track data from a GPX file and snaps it to the road network based on travel mode (walking, driving, cycling).
144+
Displays both the original GPS trace and the snapped route on an interactive vector map, along with road attributes and summary details.
145+
146+
**How it works:**
147+
Uses JavaScript and HTML to load GPX files, convert them to [GeoJSON](https://geojson.org/) using [toGeoJSON](https://github.com/mapbox/togeojson), and simplify them for optimal API performance.
148+
The [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/) aligns raw GPS points with real-world roads.
149+
Results are visualized using [MapLibre GL](https://maplibre.org/) with interactive popups for road attributes and options to download the matched route as GeoJSON or GPX.
150+
151+
**Key features:**
152+
153+
* Upload GPX file and preview the original track.
154+
* Snap raw GPS points to the nearest roads based on travel mode.
155+
* Display road attributes like road class, surface, speed limit, and lanes.
156+
* View a summary with original vs. matched distance and time.
157+
* Download snapped route as GeoJSON or GPX.
158+
159+
**APIs used:**
160+
161+
* [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/)
162+
* [Geoapify Map Tiles](https://www.geoapify.com/map-tiles/) (for base map rendering)
163+
* [MapLibre GL JS](https://maplibre.org/) (interactive map rendering)
164+
165+
**Demo:**
166+
👉 [GPX Map Matching – Live Demo](https://geoapify.github.io/maps-api-code-samples/javascript/map-matching-to-snap-gpx-to-roads-maplibre/demo_combined.html)
167+
168+
---
169+
139170
### Node.js: [Batch Geocoding with Rate Limiting](https://github.com/geoapify/maps-api-code-samples/tree/main/node/geocoding-with-RPS-limit-respect)
140171

141172
**What it does:**
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# GPX Map Matching (Snap to Roads) with Geoapify & MapLibre GL
2+
3+
This demo shows how to take GPS track data from a GPX file and snap it to the road network using the
4+
[Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/).
5+
The original GPS trace and the matched route are displayed on an interactive map powered by MapLibre GL,
6+
with travel mode selection, route summary, and download options.
7+
8+
## Features
9+
10+
- Upload a GPX file and visualize its raw GPS trace
11+
- Snap the GPS trace to the road network using the [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/)
12+
- Choose travel mode (walking, driving, cycling)
13+
- View road attributes such as road class, surface, speed limit, and lanes
14+
- Compare original and matched route distance and time
15+
- Download matched routes as **GeoJSON** or **GPX**
16+
17+
![GPX Map Matching Demo Preview](map-matching-demo-preview.jpg)
18+
19+
## Demo
20+
21+
Run the standalone demo from GitHub Pages:
22+
**[Open Demo](https://geoapify.github.io/maps-api-code-samples/javascript/map-matching-to-snap-gpx-to-roads-maplibre/demo_combined.html)**
23+
24+
## APIs and Libraries Used
25+
26+
- [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/) – snaps raw GPS traces to the road network
27+
- [MapLibre GL JS](https://maplibre.org/) – renders the interactive vector map
28+
- [toGeoJSON](https://github.com/mapbox/togeojson) – converts GPX to GeoJSON format
29+
- [togpx](https://www.npmjs.com/package/togpx) – converts GeoJSON to GPX format
30+
- [Simplify.js](https://mourner.github.io/simplify-js/) – reduces the number of GPS points for optimal performance
31+
- [Font Awesome](https://fontawesome.com/) – icons for the UI
32+
33+
## How to Run the Sample
34+
35+
You can run the demo locally or deploy it using GitHub Pages.
36+
37+
### Option 1: Run Locally with Static Server
38+
39+
1. **Install `http-server`** (if not installed globally):
40+
41+
```bash
42+
npm install -g http-server
43+
```
44+
45+
2. **Start the server** from the `src` folder:
46+
47+
```bash
48+
http-server ./src
49+
```
50+
51+
3. **Open the app** in your browser:
52+
53+
```
54+
http://localhost:8080/demo.html
55+
```
56+
57+
### Option 2: Use IDE Live Preview
58+
59+
If you use VS Code, WebStorm, or another IDE with live server support:
60+
61+
* **VS Code:** Install "Live Server" extension → Right-click `demo.html` → "Open with Live Server"
62+
* **WebStorm / IntelliJ:** Right-click file → "Open in browser"
63+
64+
> Do not open via `file://` path as it blocks dynamic imports and module loading.
65+
66+
## How to Build `demo_combined.html`
67+
68+
To create a standalone HTML file (ideal for GitHub Pages or email demos):
69+
70+
1. **Navigate to the root folder** (e.g., `javascript/map-matching-to-snap-gpx-to-roads-maplibre/`):
71+
72+
```bash
73+
cd javascript
74+
```
75+
76+
2. **Install `inline-source`** (once):
77+
78+
```bash
79+
npm install inline-source
80+
```
81+
82+
3. **Run the combine script:**
83+
84+
```bash
85+
node map-matching-to-snap-gpx-to-roads-maplibre/combine.js
86+
```
87+
88+
This will create a `demo_combined.html` with all scripts and styles inlined.
89+
90+
## Code Highlights
91+
92+
### 1. Loading a GPX File
93+
94+
When a user selects a .gpx file, the app uses the FileReader API to read it as plain text.
95+
This text contains XML data describing GPS tracks. Once loaded, the content is passed to a custom parsing function.
96+
97+
```javascript
98+
const reader = new FileReader();
99+
reader.onload = (e) => {
100+
// GPX file content as text
101+
const gpxContent = e.target.result;
102+
103+
// Convert GPX → GeoJSON and extract track points
104+
const result = fileUtils.parseGpxFile(gpxContent);
105+
106+
// ...
107+
};
108+
reader.readAsText(file);
109+
```
110+
111+
The `parseGpxFile()` helper method converts GPX XML into the widely used GeoJSON format using the toGeoJSON library:
112+
113+
```javascript
114+
parseGpxFile(gpxContent) {
115+
// Parse GPX XML content
116+
const parser = new DOMParser();
117+
const xmlDoc = parser.parseFromString(gpxContent, 'application/xml');
118+
119+
// Convert GPX → GeoJSON
120+
const geoJson = toGeoJSON.gpx(xmlDoc);
121+
122+
// Normalize and optionally simplify
123+
// ...
124+
}
125+
```
126+
127+
**Key steps:**
128+
1. Read the GPX file as text using `FileReader`.
129+
2. Parse XML into a DOM structure with `DOMParser`.
130+
3. Convert to GeoJSON using `toGeoJSON.gpx()`, which extracts track segments and points.
131+
4. Normalize track points into an array with coordinates and timestamps (if available).
132+
5. (Optional) simplify the result for optimal performance.
133+
134+
135+
### 2. Simplifying the GPX Track
136+
137+
GPX tracks can contain thousands of points, which can slow down processing and visualization.
138+
The demo uses [Simplify.js](https://mourner.github.io/simplify-js/) to reduce the number of points to a maximum of **1000** – the limit supported by the public [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/).
139+
140+
```javascript
141+
if (coordinates.length > MAX_POINTS /* 1000 */) {
142+
const result = this.simplifyPolyline(coordinates, timestamps, MAX_POINTS);
143+
coordinates = result.coordinates;
144+
timestamps = result.timestamps;
145+
console.log(`Route simplified to ${coordinates.length} points`);
146+
}
147+
```
148+
149+
**Key steps:**
150+
1. **Check track size** – If there are more than `MAX_POINTS` points (default: 1000), simplification is applied.
151+
2. **Run polyline simplification**`simplifyPolyline()` uses a tolerance-based algorithm to remove points that do not significantly affect the overall geometry.
152+
3. **Preserve timestamps** – The algorithm keeps time data aligned with the remaining points when available.
153+
4. **Result** – A simplified track that still represents the original shape but is fast enough to send to the Map Matching API (which only supports 1000 points).
154+
155+
### 3. Sending a Request to the Map Matching API
156+
157+
Once the GPS track is simplified (if needed), the points are sent to the
158+
[Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/) to “snap” them to the road network.
159+
The request includes the travel mode and an array of waypoints with coordinates and optional timestamps.
160+
161+
```javascript
162+
const requestData = {
163+
mode: travelMode, // "walk", "drive", "bicycle"
164+
waypoints: gpxData.map(point => ({
165+
location: point.location,
166+
timestamp: point.timestamp
167+
}))
168+
};
169+
170+
const response = await fetch(
171+
`https://api.geoapify.com/v1/mapmatching?apiKey=${API_KEY}`,
172+
{
173+
method: 'POST',
174+
headers: { 'Content-Type': 'application/json' },
175+
body: JSON.stringify(requestData)
176+
}
177+
);
178+
179+
const matchingResult = await response.json();
180+
```
181+
182+
**Key steps:**
183+
184+
1. **Prepare request payload** – Includes the travel mode and simplified list of track points.
185+
2. **Call Map Matching API** – Sends a POST request with JSON payload.
186+
3. **Receive snapped route** – Returns a [GeoJSON](https://geojson.org/) `FeatureCollection` containing the matched geometry and road attributes such as `road_class`, `surface`, `speed_limit`, etc.
187+
4. **Handle errors** – Checks response status and throws an error if the request fails or returns no features.
188+
189+
### 4. Visualizing the Result on the Map
190+
191+
The demo shows **both** the original GPS trace and the snapped (matched) route on an interactive
192+
[MapLibre GL](https://maplibre.org/) map.
193+
The original trace is displayed as a dashed gray line, while the matched route is drawn in blue.
194+
Hover popups show road attributes such as `road_class`, `surface`, and `speed_limit`.
195+
196+
```javascript
197+
// Original GPS trace
198+
map.addSource('gps-trace', {
199+
type: 'geojson',
200+
data: {
201+
type: 'FeatureCollection',
202+
features: [{
203+
type: 'Feature',
204+
geometry: { type: 'LineString', coordinates },
205+
properties: {}
206+
}]
207+
}
208+
});
209+
210+
map.addLayer({
211+
id: 'gps-trace-line',
212+
type: 'line',
213+
source: 'gps-trace',
214+
paint: {
215+
'line-color': '#666',
216+
'line-width': 4,
217+
'line-dasharray': [10, 5]
218+
}
219+
});
220+
221+
// Matched route from API
222+
map.addSource('matched-trace', {
223+
type: 'geojson',
224+
data: matchingResult
225+
});
226+
227+
map.addLayer({
228+
id: 'matched-trace-line',
229+
type: 'line',
230+
source: 'matched-trace',
231+
paint: {
232+
'line-color': '#007bff',
233+
'line-width': 5
234+
}
235+
});
236+
```
237+
238+
**Key steps:**
239+
240+
1. **Add original trace source and layer** – Visualize uploaded GPS track as a dashed gray line.
241+
2. **Add matched route source and layer** – Display snapped road geometry returned by the Map Matching API.
242+
3. **Highlight differences** – Compare raw vs. snapped routes visually.
243+
4. **Enable popups (optional)** – Show road attributes when hovering over the matched route.
244+
245+
### 5. Adding Popups with Road Details
246+
247+
To enhance route visualization, the demo displays **road attributes** in a popup when users hover over the matched route.
248+
Details such as `road_class`, `surface`, `speed_limit`, and `lane_count` are shown.
249+
250+
```javascript
251+
const popup = new maplibregl.Popup({ closeButton: false, closeOnClick: false });
252+
253+
map.on('mouseenter', 'matched-trace-line', (e) => {
254+
map.getCanvas().style.cursor = 'pointer';
255+
const props = e.features[0].properties;
256+
257+
popup.setLngLat(e.lngLat).setHTML(`
258+
<strong>Road class:</strong> ${props.road_class || 'n/a'}<br/>
259+
<strong>Surface:</strong> ${props.surface || 'n/a'}<br/>
260+
<strong>Speed limit:</strong> ${props.speed_limit || 'n/a'} km/h<br/>
261+
<strong>Lanes:</strong> ${props.lane_count || 'n/a'}
262+
`).addTo(map);
263+
});
264+
265+
map.on('mouseleave', 'matched-trace-line', () => {
266+
map.getCanvas().style.cursor = '';
267+
popup.remove();
268+
});
269+
```
270+
271+
**Key steps:**
272+
273+
1. **Create popup instance** – A reusable popup is created once.
274+
2. **Mouse enter event** – Shows popup with road attributes from feature properties.
275+
3. **Mouse leave event** – Hides popup and resets map cursor.
276+
277+
### 6. Downloading the Matched Route
278+
279+
The matched route can be downloaded as **GeoJSON** or **GPX** for further processing or usage in GPS devices.
280+
281+
```javascript
282+
// Download GeoJSON
283+
fileUtils.downloadGeoJSON(matchingResult);
284+
285+
// Download GPX
286+
fileUtils.downloadGPX(matchingResult, originalFileName);
287+
```
288+
289+
**Key steps:**
290+
291+
1. **GeoJSON export** – Saves snapped geometry in a standard GIS format.
292+
2. **GPX export** – Allows loading the snapped route back into GPS software or devices.
293+
294+
## Summary
295+
296+
This demo shows how to:
297+
298+
* Load GPX files and parse them into the [GeoJSON](https://geojson.org/) format
299+
* Simplify long tracks to meet the [Geoapify Map Matching API](https://www.geoapify.com/map-matching-api/) limit of 1000 points
300+
* Snap raw GPS traces to the road network based on travel mode
301+
* Visualize both original and snapped routes on an interactive [MapLibre GL](https://maplibre.org/) map
302+
* Display road attributes in interactive popups
303+
* Export the snapped route as [GeoJSON](https://geojson.org/) or [GPX](https://www.topografix.com/gpx.asp)
304+
305+
### Learn more and start building
306+
307+
* Explore [Geoapify APIs](https://www.geoapify.com/)
308+
* Read [Map Matching API documentation](https://www.geoapify.com/map-matching-api/)
309+
* Try the [live demo](https://geoapify.github.io/maps-api-code-samples/javascript/map-matching-to-snap-gpx-to-roads-maplibre/demo_combined.html)
310+
311+
**Get your free API key and start building your own map-matching apps today → [Get API Key](https://myprojects.geoapify.com)!**
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const inlineSource = require('inline-source').inlineSource;
2+
const fs = require('fs').promises;
3+
const path = require('path');
4+
5+
async function inlineResources() {
6+
const htmlPath = path.resolve('map-matching/src/demo.html');
7+
const outputPath = path.resolve('map-matching/demo_combined.html');
8+
9+
try {
10+
const html = await inlineSource(htmlPath, {
11+
compress: false,
12+
rootpath: path.resolve('map-matching/src'),
13+
ignore: []
14+
});
15+
16+
await fs.writeFile(outputPath, html, 'utf-8');
17+
console.log(`✅ Combined HTML saved to ${outputPath}`);
18+
} catch (err) {
19+
console.error(`❌ Error: ${err.message}`);
20+
}
21+
}
22+
23+
inlineResources();

0 commit comments

Comments
 (0)