Katkat

Generate a ride map image via POST API

StripeSoftware Engineer · Onsite

Last 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 <= 90 and -180 <= lon <= 180). Discard invalid coordinate points.
  • events: A list of dicts representing key stops: [{"type": str, "lat": float, "lon": float}, ...]. Valid type values are "pickup", "dropoff", and "stop".
  • max_points: The maximum number of points allowed in the polyline (max_points >= 2). If valid GPS points exceed max_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 in gps_trace and all markers in events. If lat_span = max_lat - min_lat, expand both sides by lat_span * padding_pct (clamped to [-90, 90]). Apply the same logic to longitude (clamped to [-180, 180]). If lat_span == 0, set padding to 0.01 degrees.
  • markers: All valid events, preserving their input order.
  • polyline: A list of [lat, lon] pairs derived from the downsampled valid gps_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?