|
| 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 | + |
| 15 | + |
| 16 | +## Demo |
| 17 | + |
| 18 | +Open the hosted version from GitHub Pages: |
| 19 | + |
| 20 | +[](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! |
0 commit comments