Demo
Here’s a full working demo of the planner that returns PlanResult regarding PlanRequest object.
Make sure to proto files are implemented,
Example Demo:
Section titled “Example Demo:”// Visual helpers: Get readable time textfunction hhmm(s) { if (!s) return '?'; const m = /^(\d{4}-\d{2}-\d{2})?T?(\d{2}):(\d{2})/.exec(s); return m ? `${m[2]}:${m[3]}` : s;}
// Visual helpers: Get icon regarding transit typefunction transitIcon(mode) { switch (mode) { case 0: return '🚆'; case 1: return '🚋'; case 2: return '🚌'; case 3: return '🚇'; case 4: return '⛴️'; case 5: return '🚈'; default: return '🚌'; }}
function nonTransitIcon() { return '🚶'; }
// Name getter from start of legfunction placeNameFromLegStart(l) { if (l.transitLeg) return l.transitLeg.fromEstimatedCall?.quay?.name || ''; if (l.nonTransitLeg) return l.nonTransitLeg.fromPlace?.name || ''; if (l.flexibleLeg) return l.flexibleLeg.fromPlace?.name || ''; return '';}
// Name getter from end of legfunction placeNameFromLegEnd(l) { if (l.transitLeg) return l.transitLeg.toEstimatedCall?.quay?.name || ''; if (l.nonTransitLeg) return l.nonTransitLeg.toPlace?.name || ''; if (l.flexibleLeg) return l.flexibleLeg.toPlace?.name || ''; return '';}
// Summarize Trip logic visualizedfunction summarizeTrip(t) { const legs = t.legs || []; const startTime = t.aimedStartTime || legs[0]?.aimedStartTime || t.expectedStartTime; const endTime = t.aimedEndTime || legs[legs.length - 1]?.aimedEndTime || t.expectedEndTime; const startName = legs.length ? (placeNameFromLegStart(legs[0]) || 'Start') : 'Start'; const endName = legs.length ? (placeNameFromLegEnd(legs[legs.length - 1]) || 'End') : 'End';
const header = `🕒 ${hhmm(startTime)} → ${hhmm(endTime)} • ${t.duration ?? '?'}m • ${t.transfers ?? 0} transfer${(t.transfers||0) === 1 ? '' : 's'}`; const sub = `📍 ${startName} → ${endName}`;
const legLines = legs.map((l) => { if (l.transitLeg) { const icon = transitIcon(l.transitLeg.mode); const line = l.transitLeg.line?.publicCode || l.transitLeg.line?.name || l.transitLeg.serviceJourney?.publicCode || 'Transit'; const from = placeNameFromLegStart(l) || '—'; const to = placeNameFromLegEnd(l) || '—'; const plat = l.transitLeg.fromEstimatedCall?.aimedPlatform ? ` (plat. ${l.transitLeg.fromEstimatedCall.aimedPlatform})` : ''; return ` - ${icon} ${line} ${hhmm(l.aimedStartTime)} → ${hhmm(l.aimedEndTime)} ${from}${plat} → ${to}`; } if (l.nonTransitLeg) { const icon = nonTransitIcon(l.nonTransitLeg.mode); const dist = l.distance != null ? ` • ${(l.distance/1000).toFixed(1)} km` : ''; const dur = l.duration != null ? ` • ${l.duration}m` : ''; const from = placeNameFromLegStart(l) || ''; const to = placeNameFromLegEnd(l) || ''; const hop = from && to ? ` ${from} → ${to}` : ''; return ` - ${icon} ${hhmm(l.aimedStartTime)} → ${hhmm(l.aimedEndTime)}${dur}${dist}${hop}`; } if (l.waitingLeg) { return ` - ⏱️ wait ${hhmm(l.waitingLeg.startTime)} → ${hhmm(l.waitingLeg.endTime)}`; } if (l.flexibleLeg) { const line = l.flexibleLeg.line?.publicCode || 'on-demand'; const from = placeNameFromLegStart(l) || '—'; const to = placeNameFromLegEnd(l) || '—'; return ` - 🚐 ${line} ${hhmm(l.aimedStartTime)} → ${hhmm(l.aimedEndTime)} ${from} → ${to}`; } return ` - leg ${hhmm(l.aimedStartTime)} → ${hhmm(l.aimedEndTime)} (${l.duration ?? '?'}m)`; });
return [header, sub, ...legLines].join('\n');}
// Client side & Connection(async () => { // Build types const reqRoot = protobuf.parse(requestProto).root; const PlanRequest = reqRoot.lookupType('planner.PlanRequest'); const UserSpeed = reqRoot.lookupEnum('planner.UserPreferences.UserSpeed').values;
const resRoot = protobuf.parse(planResultProto).root; const PlanResult = resRoot.lookupType('planner.model.PlanResult');
// Build request const reqObj = { fromPlace: '52.3702,4.8952', toPlace: '52.0907,5.1214', timestamp: new Date().toISOString(), arriveBy: false, userPreferences: { walkingSpeed: UserSpeed.AVERAGE, bikingSpeed: UserSpeed.AVERAGE, hasEbike: false } }; const err = PlanRequest.verify(reqObj); if (err) throw new Error(err); const binReq = PlanRequest.encode(PlanRequest.create(reqObj)).finish();
// Connect + send const url = 'wss://api.planner.moopmoop.com/ws/plannermixer/proto'; const ws = new WebSocket(url, { headers: { 'Content-Type': 'application/protobuf' } });
ws.on('open', () => { console.log(`[open] sending PlanRequest (${binReq.length} bytes)`); ws.send(binReq, { binary: true }); });
ws.on('message', (data, isBinary) => { const buf = isBinary ? data : Buffer.from(data); const txt = tryText(buf);
if (txt) { console.log(`[server JSON] ${txt}`); return; }
const result = PlanResult.decode(buf); console.log(`timestamp: ${result.timestamp || 'n/a'}, planner: ${result.planner}, grade: ${result.grade || 'n/a'}`);
if (result.errors && result.errors.length) { console.log('Errors:', result.errors); }
const trips = result.trips || []; if (!trips.length) { console.log('(no trips)'); } else { trips.forEach((t, i) => { console.log(`\nTrip ${i + 1}/${trips.length}`); console.log(summarizeTrip(t)); }); } });
ws.on('close', (code, reason) => { console.log(`[close] ${code} ${reason?.toString() || ''}`); });
ws.on('error', (e) => { console.error('[error]', e); });
function tryText(b) { try { const s = b.toString('utf8'); return /^\s*[{[]/.test(s) ? s : null; } catch { return null; } }})();