import {parse} from 'qs'; import get from 'lodash/get.js'; const POI = 'P'; const STATION = 'S'; const ADDRESS = 'A'; const leadingZeros = /^0+/; // todo: what is l.wt? is it "weight"? // - `6733` for 8013074 with p/vmt // - `3933` for 8012092 with p/vmt // - `2062` for 8010168 with p/vmt // todo: l.gidL (e.g. `["A×de:15088:8013414"]`) // todo: `i` param in `lid` (e.g. `A=1@O=Zöberitz@X=12033455@Y=51504612@U=80@L=8013414@i=A×de:15088:8013414@`) const parseLocation = (ctx, l) => { const {profile, opt} = ctx; const lid = parse(l.lid, {delimiter: '@'}); const res = { type: 'location', id: (l.extId || lid.L || '').replace(leadingZeros, '') || null, }; if (l.crd) { res.latitude = l.crd.y / 1000000; res.longitude = l.crd.x / 1000000; } else if ('X' in lid && 'Y' in lid) { res.latitude = lid.Y / 1000000; res.longitude = lid.X / 1000000; } if (l.type === STATION) { // todo: https://github.com/public-transport/hafas-client/issues/151 const locL = get(ctx.res, ['common', 'locL'], []); const mMastLocX = 'mMastLocX' in l ? l.mMastLocX : NaN; const subStops = (l.stopLocL || []) .filter(locX => locX !== mMastLocX) .map(locX => locL[locX]) .filter(s => Boolean(s)) .map(s => profile.parseLocation(ctx, s)) .filter(stop => Boolean(stop)); const stop = { type: l.isMainMast || subStops.length > 0 ? 'station' : 'stop', id: res.id, name: l.name || lid.O ? profile.parseStationName(ctx, l.name || lid.O) : null, location: 'number' === typeof res.latitude ? res : null, // todo: remove `.id` }; if (opt.subStops && subStops.length > 0) { stop.stops = subStops; } if ('pCls' in l) { stop.products = profile.parseProductsBitmask(ctx, l.pCls); } if ('meta' in l) { stop.isMeta = Boolean(l.meta); } const mMastLoc = locL[mMastLocX]; if (mMastLoc) { stop.station = { ...profile.parseLocation(ctx, mMastLoc), type: 'station', // todo: this should be handled differently }; } if (opt.entrances) { const entrances = (l.entryLocL || []) .map(locX => locL[locX]) .filter(l => Boolean(l)) .map(l => profile.parseLocation(ctx, l)) .filter(loc => Boolean(loc)) .map(loc => loc.location); if (entrances.length > 0) { stop.entrances = entrances; } } if (opt.linesOfStops && Array.isArray(l.lines)) { stop.lines = l.lines; } const locHints = (l.remarkRefs || []) .filter(ref => Boolean(ref.hint) && Array.isArray(ref.tagL)) .filter(({tagL}) => tagL.includes('RES_LOC') || tagL.find(t => t.slice(0, 8) === 'RES_LOC_'), // e.g. `RES_LOC_H3` ) .map(ref => ref.hint); const hints = [ ...l.hints || [], ...locHints, ]; const byType = type => hints.find(h => h.type === type); const transitAuthority = (byType('transit-authority') || {}).text; if (transitAuthority) { stop.transitAuthority = transitAuthority; } const dhid = (byType('stop-dhid') || {}).text; if (dhid) { if (!stop.ids) { stop.ids = {}; } stop.ids.dhid = dhid; } const otherIds = hints .filter(h => h.type === 'foreign-id') .filter(h => 'string' === typeof h.text && h.text.includes(':')) .map(({text}) => { const i = text.indexOf(':'); return [text.slice(0, i), text.slice(i + 1)]; }) .filter(([src]) => src !== 'NULL'); if (otherIds.length > 0) { if (!stop.ids) { stop.ids = {}; } for (const [src, id] of otherIds) { stop.ids[src] = id; } } return stop; } if (l.type === ADDRESS) { res.address = l.name; } else { res.name = l.name; } if (l.type === POI) { res.poi = true; } return res; }; // We use a "visited list" to prevent endless recursion. // todo: can we use a WeakMap here? const seen = Symbol('parseLocation seen items'); const parseLocationWithoutCycles = (ctx, l, ...args) => { if (ctx[seen] && ctx[seen].includes(l)) { return null; } const newSeen = ctx[seen] ? [...ctx[seen], l] : [l]; return parseLocation({...ctx, [seen]: newSeen}, l, ...args); }; export { parseLocationWithoutCycles as parseLocation, };