--- title: Map Delivery Coverage Zones | Plaza Docs description: An interactive map that generates driving isochrones around a restaurant and displays tiered delivery fee zones with colored overlays. --- Delivery apps charge different fees based on how far away you are. That “how far” isn’t straight-line distance — it’s drive time. A restaurant 2 miles away across a bridge might take 20 minutes, while one 4 miles away on a highway takes 10. This app takes a restaurant address, generates 10/20/30 minute driving isochrones, and overlays them on a map as colored delivery zones with tiered pricing. ## What you’ll use - **Geocoding** to turn the restaurant address into coordinates - **Isochrones** to generate drive-time polygons at 10, 20, and 30 minutes - **MapLibre GL JS** to render the zones as colored layers ## Key code ### Fetching isochrones One API call returns all three contours. Pass comma-separated minute values and get back a GeoJSON FeatureCollection with one polygon per contour: lib/plaza.js ``` import Plaza from "@plazafyi/sdk"; const client = new Plaza(); export async function getIsochrones(lat, lng) { return client.v1.isochrone({ lat, lng, contours_minutes: "10,20,30", mode: "auto", }); } ``` ### Zone configuration with tiered fees Each contour maps to a delivery tier. Define the zones once and use them for both the map layers and the legend: ``` const ZONES = [ { minutes: 10, color: "rgba(5,150,105,0.35)", stroke: "#059669", fee: "Free" }, { minutes: 20, color: "rgba(245,158,11,0.35)", stroke: "#f59e0b", fee: "$5" }, { minutes: 30, color: "rgba(239,68,68,0.35)", stroke: "#ef4444", fee: "$10" }, ]; ``` ### Adding GeoJSON layers to MapLibre Sort features largest-first so smaller zones paint on top, creating a bullseye effect. Each zone gets a fill layer and a stroke layer: ``` // Sort descending so largest zone draws first (smallest on top) const sorted = [...features].sort((a, b) => { return (b.properties?.contour ?? 0) - (a.properties?.contour ?? 0); }); sorted.forEach((feature) => { const contour = feature.properties?.contour ?? 0; const zone = ZONES.find((z) => z.minutes === contour); if (!zone) return; const id = `zone-${ZONES.indexOf(zone)}`; map.addSource(id, { type: "geojson", data: { type: "FeatureCollection", features: [feature] }, }); map.addLayer({ id: `${id}-fill`, type: "fill", source: id, paint: { "fill-color": zone.color }, }); map.addLayer({ id: `${id}-line`, type: "line", source: id, paint: { "line-color": zone.stroke, "line-width": 2 }, }); }); ``` ## How it works **Geocode** turns the address into coordinates. **Multi-contour isochrone** — instead of three separate requests, pass comma-separated minute values: `contours_minutes=10,20,30`. Plaza returns a FeatureCollection with one polygon per contour, each representing the area reachable by car within that time. **Layer ordering** — add the 30-minute zone first, then 20, then 10. MapLibre draws layers in order, so the smaller zones paint on top. **Fee lookup** — in production, geocode the customer’s address and check which polygon contains it. MapLibre handles this client-side with `map.queryRenderedFeatures()`, or you can do a server-side point-in-polygon check. | Zone | Drive Time | Delivery Fee | | ----------- | ----------- | ------------ | | Green | 0 — 10 min | Free | | Yellow | 10 — 20 min | $5.00 | | Red | 20 — 30 min | $10.00 | | Outside red | 30+ min | No delivery | ## Full source The complete app is at [github.com/plazafyi/example-apps/tree/main/delivery-zones](https://github.com/plazafyi/example-apps/tree/main/delivery-zones). Built with Next.js + MapLibre GL JS. ## Variations to try **Walking zones for a neighborhood restaurant.** Switch `profile` to `foot` and use shorter times: `contours_minutes=5,10,15`. Great for lunch spots. **Bike courier delivery.** Use `profile=bicycle` with times like `10,15,20`. Bike couriers cover more ground than walking but follow different routes than cars. **Dynamic pricing.** Instead of fixed tiers, calculate the fee as a function of drive time: `fee = Math.max(0, (driveMinutes - 10) * 0.50)`. Show the estimated fee in a popup when a customer clicks their location. **Click-to-check.** Add a click handler that tells you which zone a point falls in: ``` map.on("click", (e) => { const features = map.queryRenderedFeatures(e.point, { layers: ["zone-0-fill", "zone-1-fill", "zone-2-fill"], }); if (features.length) { const zoneIndex = parseInt(features[0].layer.id.split("-")[1]); new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(`Delivery fee: ${ZONES[zoneIndex].fee}`) .addTo(map); } }); ```