Skip to content

Export Engine

The export engine is the most technically significant part of Apollo Map Studio. It must produce binary files identical in structure to those generated by Apollo's C++ tools.

Source files ported

Apollo C++ sourceStudio moduleFidelity
modules/map/tools/sim_map_generator.ccexport/buildSimMap.tsAlgorithm-exact port
modules/routing/topo_creator/node_creator.ccexport/buildRoutingMap.tsFormula-exact port
modules/routing/topo_creator/edge_creator.ccexport/buildRoutingMap.tsFormula-exact port
modules/routing/conf/routing_config.pb.txtexport/buildRoutingMap.ts (constants)Values copied

buildBaseMap

Lane → proto translation

GeoJSON LineString (WGS84)

  ▼  per coordinate: lngLatToENU(lng, lat)
CurveSegment.lineSegment.point[] (PointENU)

  ▼  turf.length(centerLine) in meters
Lane.length

  ▼  turf.lineOffset(centerLine, ±width/2, {units:'meters'})
Left/right boundary CurveSegment

  ▼  turf.along(centerLine, s, {units:'meters'}) for s = 0,1,2,...,length
LaneSampleAssociation[] (left_sample, right_sample)

Each LaneSampleAssociation stores { s, width } where width is the perpendicular distance from the centerline to the boundary at position s.

Overlap algorithm

ts
for each lane:
  for each crosswalk:
    intersections = turf.lineIntersect(lane.centerLine, polygonToLine(crosswalk.polygon))
    if intersections.length > 0:
      s_list = intersections.map(pt => nearestPointOnLine(lane, pt).location * lane.length)
      startS = min(s_list), endS = max(s_list)
      emit Overlap { lane(startS,endS), crosswalk }

  for each signal:
    intersection = turf.lineIntersect(lane.centerLine, signal.stopLine)
    if found:
      s = nearestPointOnLine(lane, intersection[0]).location * lane.length
      emit Overlap { lane(s-0.5, s+0.5), signal }

  // Same pattern for stop signs, speed bumps

  for each junction:
    midpoint = centerLine.coordinates[floor(n/2)]
    if booleanPointInPolygon(midpoint, junction.polygon):
      emit Overlap { lane(0, lane.length), junction }

buildSimMap

Downsampling algorithm (port of points_downsampler.h)

ts
function downsampleByAngle(points: number[][], threshold = Math.PI / 180): number[][] {
  const result = [points[0]]
  for (let i = 1; i < points.length - 1; i++) {
    const h1 = bearing(points[i - 1], points[i])
    const h2 = bearing(points[i], points[i + 1])
    const delta = Math.abs(h2 - h1)
    if (delta >= threshold) result.push(points[i])
  }
  result.push(points[points.length - 1])
  return result
}

function downsampleByDistance(points: number[][], interval = 5, steepInterval = 1): number[][] {
  const result = [points[0]]
  let accumulated = 0
  for (let i = 1; i < points.length; i++) {
    const d = haversineMeters(points[i - 1], points[i])
    accumulated += d
    const h1 = bearing(points[i - 1], points[i])
    const h2 = i < points.length - 1 ? bearing(points[i], points[i + 1]) : h1
    const delta = Math.abs(h2 - h1)
    const threshold = delta > Math.PI / 4 ? steepInterval : interval
    if (accumulated >= threshold) {
      result.push(points[i])
      accumulated = 0
    }
  }
  if (result[result.length - 1] !== points[points.length - 1]) {
    result.push(points[points.length - 1])
  }
  return result
}

Both passes are applied to central_curve, left_boundary, and right_boundary of every lane.

buildRoutingMap

Full algorithm

ts
// 1. One node per lane
for each lane:
  turnPenalty = { NO_TURN: 0, LEFT_TURN: 50, RIGHT_TURN: 20, U_TURN: 100 }[lane.turn]
  nodeCost = lane.length * Math.sqrt(BASE_SPEED / lane.speedLimit) + turnPenalty

  isVirtual = (lane.junctionId != null)
           && (lane.leftNeighborIds.length === 0)
           && (lane.rightNeighborIds.length === 0)

  nodes.push({ laneId, length, cost: nodeCost, isVirtual, roadId })

// 2. FORWARD edges (successor relationships)
for each lane:
  for each successorId in lane.successorIds:
    edges.push({ fromLaneId: lane.id, toLaneId: successorId,
                 cost: 0, direction: FORWARD })

// 3. LATERAL edges (neighbor lane changes)
for each lane:
  for side in ['left', 'right']:
    neighborIds = side === 'left' ? lane.leftNeighborIds : lane.rightNeighborIds
    sharedBoundary = side === 'left' ? lane.leftBoundaryType : lane.rightBoundaryType

    if sharedBoundary in [DOTTED_WHITE, DOTTED_YELLOW]:
      changingLength = BASE_CHANGING_LENGTH  // 50 m
      cost = CHANGE_PENALTY * Math.pow(changingLength / BASE_CHANGING_LENGTH, -1.5)
      // = 500 * (50/50)^-1.5 = 500

      for each neighborId in neighborIds:
        direction = side === 'left' ? LEFT : RIGHT
        edges.push({ fromLaneId: lane.id, toLaneId: neighborId, cost, direction })

Configuration constants

From modules/routing/conf/routing_config.pb.txt:

base_speed:            4.167   // m/s (15 km/h)
left_turn_penalty:     50
right_turn_penalty:    20
uturn_penalty:         100
change_penalty:        500
base_changing_length:  50      // meters

Protobuf encoding

Both buildBaseMap and buildRoutingMap return plain JavaScript objects whose shape matches the proto definitions. codec.ts passes them to protobufjs for encoding:

ts
export async function encodeMap(mapObj: ApolloMap): Promise<Uint8Array> {
  const root = await loadProtoRoot('map.proto')
  const MapType = root.lookupType('apollo.hdmap.Map')
  const err = MapType.verify(mapObj)
  if (err) throw new Error(`Map verify failed: ${err}`)
  return MapType.encode(MapType.create(mapObj)).finish()
}

The resulting Uint8Array is wrapped in a Blob and downloaded via a temporary <a> element.

Released under the MIT License.