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++ source | Studio module | Fidelity |
|---|---|---|
modules/map/tools/sim_map_generator.cc | export/buildSimMap.ts | Algorithm-exact port |
modules/routing/topo_creator/node_creator.cc | export/buildRoutingMap.ts | Formula-exact port |
modules/routing/topo_creator/edge_creator.cc | export/buildRoutingMap.ts | Formula-exact port |
modules/routing/conf/routing_config.pb.txt | export/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 // metersProtobuf 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.