Generate a ride map image via POST API
StripeSoftware Engineer · OnsiteLast updated
Implement a payload builder for an external static map generation API that processes raw ride GPS telemetry, downsamples points, generates bounding viewports, and formats marker overlays.
Problem Statement
Ride-hailing receipts require a static map image displaying the completed route along with pickup and dropoff markers. You are integrating with a third-party mapping API that accepts map specifications via a POST request.
Write a function build_map_payload that takes raw GPS telemetry and ride event markers, validates the coordinates, downsamples the trajectory if it exceeds the API's maximum coordinate limit, computes an expanded bounding box viewport, and formats the output dictionary ready for serialization to JSON.
Function Signature
def build_map_payload(
gps_trace: list[dict],
events: list[dict],
max_points: int = 100,
padding_pct: float = 0.1
) -> dict:
pass
Input
gps_trace: A list of dicts ordered by timestamp:[{"lat": float, "lon": float, "timestamp": int}, ...]. Coordinates must be valid (-90 <= lat <= 90and-180 <= lon <= 180). Discard invalid coordinate points.events: A list of dicts representing key stops:[{"type": str, "lat": float, "lon": float}, ...]. Validtypevalues are"pickup","dropoff", and"stop".max_points: The maximum number of points allowed in the polyline (max_points >= 2). If valid GPS points exceedmax_points, downsample the list uniformly while strictly preserving the first and last points.padding_pct: Percentage expansion applied to the bounding box dimensions (padding_pct >= 0.0).
Output Format
Return a dictionary with the following schema:
{
"viewport": {
"min_lat": float,
"min_lon": float,
"max_lat": float,
"max_lon": float
},
"markers": [
{"type": str, "lat": float, "lon": float}
],
"polyline": [
[float, float]
]
}
viewport: The bounding box enclosing all valid points ingps_traceand all markers inevents. Iflat_span = max_lat - min_lat, expand both sides bylat_span * padding_pct(clamped to[-90, 90]). Apply the same logic to longitude (clamped to[-180, 180]). Iflat_span == 0, set padding to0.01degrees.markers: All valid events, preserving their input order.polyline: A list of[lat, lon]pairs derived from the downsampled validgps_trace.
If there are no valid GPS points and no valid events, raise a ValueError.
Example
gps_trace = [
{"lat": 37.7749, "lon": -122.4194, "timestamp": 1609459200},
{"lat": 37.7755, "lon": -122.4180, "timestamp": 1609459230},
{"lat": 37.7760, "lon": -122.4170, "timestamp": 1609459260},
{"lat": 37.7770, "lon": -122.4160, "timestamp": 1609459290},
]
events = [
{"type": "pickup", "lat": 37.7749, "lon": -122.4194},
{"type": "dropoff", "lat": 37.7770, "lon": -122.4160}
]
result = build_map_payload(gps_trace, events, max_points=3, padding_pct=0.1)
Downsampled polyline has 3 points (indexes 0, 1 or 2, and 3):
# result["polyline"] -> [[37.7749, -122.4194], [37.7755, -122.418], [37.777, -122.416]]
# (or index 2 depending on rounding, as long as endpoints are preserved and points are evenly spaced)
Hints
Hint 1
Filter out invalid coordinates from both `gps_trace` and `events` first before computing the viewport or downsampling.
Hint 2
For uniform downsampling of size N down to K points, pick indices using `round(i * (N - 1) / (K - 1))` for `i` from 0 to `K - 1`.
Hint 3
Make sure the bounding box computation includes coordinates from both the valid GPS trace and the event markers, then clamp the padded coordinates within valid lat/lon ranges.
Follow-up questions
- How would you replace uniform index-based downsampling with the Ramer-Douglas-Peucker (RDP) algorithm to better preserve sharp turns in the route?
- If the external map API occasionally fails or rate-limits requests, how would you design an asynchronous background generation system with retry policies and idempotency?
- How would you handle routes crossing the antimeridian (180th meridian) when computing bounding boxes?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.