Skip to content

Commit 853fb4a

Browse files
author
Alfiya Tarasenko
committed
Add nearest supermarkets code sample - Places API + Route Matrix API + Routing API
1 parent a991e03 commit 853fb4a

12 files changed

Lines changed: 3275 additions & 1 deletion

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"activityBar.background": "#142868",
44
"titleBar.activeBackground": "#1C3892",
55
"titleBar.activeForeground": "#FAFBFE"
6-
}
6+
},
7+
"liveServer.settings.port": 5501
78
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Nearest POIs by Driving Time with Geoapify & MapLibre GL
2+
3+
This JavaScript sample shows how to click on any location, fetch nearby supermarkets, and rank them by **actual travel time** instead of straight-line distance. It combines the Geoapify Places, Route Matrix, and Routing APIs with a polished MapLibre GL UI so that users can explore the ten fastest supermarkets as an example of POI for any travel mode and instantly visualize routes.
4+
5+
## Features
6+
7+
- Set a user location by clicking on the map (default location - Berlin)
8+
- Retrieve supermarkets from the [Geoapify Places API](https://www.geoapify.com/places-api/) within a configurable radius (by default is 3 km)
9+
- Get driving time and distance from the user location for every POI using the [Geoapify Route Matrix API](https://www.geoapify.com/route-matrix-api/)
10+
- Highlight the top 10 closest supermarkets with numbered markers and synchronized sidebar cards
11+
- Request a detailed route for any result via the [Geoapify Routing API](https://www.geoapify.com/routing-api/) and display it on the map
12+
- Responsive layout with loading states, error handling, and route fit-to-bounds logic
13+
14+
![Nearest Supermarkets Demo Preview](nearest-supermarkets-demo-preview.png)
15+
16+
## Demo
17+
18+
Open the hosted version from GitHub Pages:
19+
20+
[![Launch Demo](https://img.shields.io/badge/Launch%20Demo-View%20Sample-007bff?style=for-the-badge)](https://geoapify.github.io/maps-api-code-samples/javascript/nearest-poi-get-places-sorted-by-driving-time/demo_combined.html)
21+
22+
## APIs and Libraries Used
23+
24+
- [Geoapify Places API](https://www.geoapify.com/places-api/) – finds supermarkets around the clicked point
25+
- [Geoapify Map Marker API](https://www.geoapify.com/map-marker-icon-api/) – generates the custom numbered markers displayed on the map
26+
- [Geoapify Route Matrix API](https://www.geoapify.com/route-matrix-api/) – calculates travel time and distance for multiple destinations and modes
27+
- [Geoapify Routing API](https://www.geoapify.com/routing-api/) – returns a complete itinerary to a selected supermarket
28+
- [MapLibre GL JS](https://maplibre.org/) – renders the interactive basemap, markers, and routes
29+
- [inline-source](https://www.npmjs.com/package/inline-source) – flattens the demo into a single HTML file for easy hosting
30+
31+
## How to Run the Sample Locally
32+
33+
You can develop from the `src` folder or host a combined HTML build.
34+
35+
### Option 1: Run Locally with a Static Server
36+
37+
1. Install a lightweight server (once):
38+
```bash
39+
npm install -g http-server
40+
```
41+
2. Serve the `src` folder:
42+
```bash
43+
cd javascript/nearest-poi-get-places-sorted-by-driving-time/src
44+
http-server .
45+
```
46+
3. Open in your browser:
47+
```
48+
http://localhost:8080/demo.html
49+
```
50+
51+
### Option 2: Use IDE Live Preview
52+
53+
- **VS Code:** Install *Live Server* → right-click `demo.html` → “Open with Live Server”
54+
- **WebStorm / IntelliJ:** Right-click `demo.html` → “Open in Browser”
55+
56+
> Avoid opening via a `file://` path—the module-based helper imports require an HTTP server.
57+
58+
## How to Build `demo_combined.html`
59+
60+
Generate a standalone HTML file with all resources inlined (ideal for GitHub Pages or newsletters):
61+
62+
```bash
63+
cd javascript
64+
npm install inline-source
65+
node nearest-poi-get-places-sorted-by-driving-time/combine.js
66+
```
67+
68+
The script reads `src/demo.html`, inlines linked CSS/JS, and writes `demo_combined.html` to the project root.
69+
Use `demo_combined.html` whenever you need a single drop-in file for static hosting, demos, or sharing with stakeholders who should not handle multiple assets.
70+
71+
## Code Highlights
72+
73+
### 1. Query POI with Places API (`src/helper/places-api.js`)
74+
75+
This helper builds a [Geoapify Places API](https://www.geoapify.com/places-api/) query that filters by the `commercial.supermarket` category, adds a `circle` filter, and biases results toward the clicked location—an SEO-friendly pattern whenever you want to highlight a specific POI vertical (cafés, EV chargers, pharmacies, etc.) in your content:
76+
77+
```javascript
78+
const params = new URLSearchParams({
79+
categories: 'commercial.supermarket',
80+
filter: `circle:${location.lng},${location.lat},${radius}`,
81+
bias: `proximity:${location.lng},${location.lat}`,
82+
limit,
83+
apiKey: this.apiKey
84+
});
85+
86+
const response = await fetch(`${this.baseUrl}?${params}`);
87+
```
88+
89+
Example request URL (with placeholders filled in):
90+
91+
```
92+
https://api.geoapify.com/v2/places?categories=commercial.supermarket&filter=circle:13.405,52.52,3000&bias=proximity:13.405,52.52&limit=20&apiKey=YOUR_API_KEY
93+
```
94+
95+
- `categories` — selects the exact POI vertical you want to surface (great for SEO because it matches user intent); browse the full taxonomy in the [Geoapify category list](https://apidocs.geoapify.com/docs/places/#categories)
96+
- `filter=circle:<lon>,<lat>,<radius>` — hard-limits the search area so results stay relevant to the clicked map location
97+
- `bias=proximity:<lon>,<lat>` — orders features by proximity when multiple POIs fall inside the same circle
98+
- `limit` — caps the number of returned features; this sample grabs more than 10 to later sort them by travel time
99+
- `apiKey` — your Geoapify project key
100+
101+
Adding a filter (circle, bounding box, polygon) is recommended even if you already bias the query, because it keeps the dataset small, reduces latency, and prevents unrelated POIs from creeping into SEO-focused landing pages or demos.
102+
103+
The `processSupermarkets()` method then converts each GeoJSON feature into the internal structure used by the UI (id, name, formatted address, coordinates, categories, and contact info).
104+
105+
### 2. Get POI travel time & distance with Route Matrix (`src/helper/route-matrix-api.js`)
106+
107+
The sample requests more than ten supermarkets, sends them as `targets`, and lets the Route Matrix API return drive times/distances for the selected travel mode—perfect for “nearest POI by travel time” scenarios:
108+
109+
```javascript
110+
const requestBody = {
111+
mode,
112+
sources: [{ location: [origin.lng, origin.lat] }],
113+
targets: destinations.map(dest => ({
114+
location: [dest.coordinates.lng, dest.coordinates.lat]
115+
}))
116+
};
117+
118+
const response = await fetch(`${this.baseUrl}?apiKey=${this.apiKey}`, {
119+
method: 'POST',
120+
headers: { 'Content-Type': 'application/json' },
121+
body: JSON.stringify(requestBody)
122+
});
123+
```
124+
125+
Example request:
126+
127+
```
128+
POST https://api.geoapify.com/v1/routematrix?apiKey=YOUR_API_KEY
129+
Content-Type: application/json
130+
131+
{
132+
"mode": "drive",
133+
"sources": [{ "location": [13.405, 52.52] }],
134+
"targets": [
135+
{ "location": [13.39, 52.515] },
136+
{ "location": [13.42, 52.525] }
137+
]
138+
}
139+
```
140+
141+
- `mode` — routing profile (walk, drive, truck, etc.), matching the user’s selection
142+
- `sources` — origin coordinates; this sample uses the user’s location as a single source
143+
- `targets` — list of POI coordinates returned by the Places API
144+
- `apiKey` — Geoapify project key
145+
146+
`processMatrixResults()` pairs each matrix cell with the supermarket, formats seconds/meters into friendly strings, and sorts the list by ascending travel time so you can show “Top 10 POIs by travel time” in the UI.
147+
148+
### 3. Get route geometry with the Routing API (`src/helper/routing-api.js`)
149+
150+
When a user taps “Show Route,” the app calls the Routing API with waypoint coordinates and the currently selected mode to fetch the full route geometry (LineString/MultiLineString) plus travel stats:
151+
152+
```javascript
153+
const params = new URLSearchParams({
154+
waypoints: `${origin.lat},${origin.lng}|${destination.coordinates.lat},${destination.coordinates.lng}`,
155+
mode,
156+
apiKey: this.apiKey
157+
});
158+
159+
const response = await fetch(`${this.baseUrl}?${params}`);
160+
```
161+
162+
Example request:
163+
164+
```
165+
GET https://api.geoapify.com/v1/routing?waypoints=52.52,13.405|52.515,13.39&mode=drive&apiKey=YOUR_API_KEY
166+
```
167+
168+
- `waypoints``lat,lon|lat,lon` pairs that describe the origin and destination (add more pairs for multi-stop routes)
169+
- `mode` — routing profile; determines which network, speeds, and road restrictions to honor
170+
- `apiKey` — Geoapify project key
171+
172+
The response is a FeatureCollection whose first feature contains:
173+
174+
- `geometry` — the route geometry (suitable for MapLibre, Leaflet, etc.)
175+
- `properties.distance` — meters
176+
- `properties.time` — seconds
177+
- `properties.mode` and `properties.waypoints` — metadata about the request
178+
179+
`MapHelper.displayRoute()` takes the returned route geometry, applies mode-specific styling, and `fitToRoute()` iterates through the coordinates to compute map bounds for a polished zoom-to-route animation.
180+
181+
### 4. MapLibre GL methods used in the helpers
182+
183+
| MapLibre method | What it does for this sample |
184+
| --- | --- |
185+
| `new maplibregl.Map({ ... })` | Creates the MapLibre GL instance with the Geoapify style URL, centered on Berlin, and hosts all subsequent layers/sources. |
186+
| `map.loadImage(url)` | Downloads the numbered marker sprites generated by the Map Marker API so they can be displayed as MapLibre icons. |
187+
| `map.addImage(name, image)` | Registers each downloaded sprite (rank 1–10 and the user-location icon) so symbol layers can reference them. |
188+
| `map.addSource(id, { type: 'geojson', data })` | Adds GeoJSON sources for user location, supermarkets, and routes; these sources are updated whenever new POIs or routes arrive. |
189+
| `map.addLayer({ ... })` | Creates symbol and line layers that visualize the GeoJSON sources (ranked markers, user icon, and route line). |
190+
| `map.setLayoutProperty()/setPaintProperty()` | Dynamically tweaks icon size/opacity or line color/width to highlight selections and switch styles per travel mode. |
191+
| `map.fitBounds(bounds, options)` | Animates the map to show both the user marker and the selected POIs (or the entire route geometry) within the viewport. |
192+
| `map.on('click'/'mouseenter'/'mouseleave', handler)` | Powers the interactive behavior: clicking on the map sets a new origin, while pointer events on markers trigger sidebar highlighting. |
193+
194+
## Summary
195+
196+
- Use Geoapify Places + Route Matrix to discover the closest supermarkets (or any POI category) by travel time
197+
- Let users switch travel modes, instantly re-ranking the results by the latest time/distance matrix
198+
- Render beautiful numbered markers via the Map Marker API, sync them with sidebar cards, and keep hover/click states aligned
199+
- Offer single-click routing with detailed stats and polished map animations so users can navigate right away
200+
201+
### Learn More and Build Your Own
202+
203+
- Explore the full [Geoapify API suite](https://www.geoapify.com/)
204+
- Review the [Places API docs](https://www.geoapify.com/places-api/), [Route Matrix API docs](https://www.geoapify.com/route-matrix-api/), and [Map Marker API docs](https://www.geoapify.com/map-marker-icon-api/)
205+
- Create your free API key at [myprojects.geoapify.com](https://myprojects.geoapify.com) and start building your “nearest POI by travel time” experience today!
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('nearest-poi-get-places-sorted-by-driving-time/src/demo.html');
7+
const outputPath = path.resolve('nearest-poi-get-places-sorted-by-driving-time/demo_combined.html');
8+
9+
try {
10+
const html = await inlineSource(htmlPath, {
11+
compress: false,
12+
rootpath: path.resolve('nearest-poi-get-places-sorted-by-driving-time/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)