File indexing completed on 2024-12-29 04:51:03

0001 /*
0002    SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 function isHeaderOrFooter(line) {
0008     return line.search(/(Ihre Reiseverbindung|Wichtige Nutzungshinweise|Hinweise:|Seite \d \/ \d)/) >= 0;
0009 }
0010 
0011 function parseSeat(res, text) {
0012     var coach = text.match(/Wg. (\d+)/);
0013     if (coach)
0014         res.reservedTicket.ticketedSeat.seatSection = coach[1];
0015     var seat = text.match(/Pl. ([\d ]+\d)/);
0016     if (seat)
0017         res.reservedTicket.ticketedSeat.seatNumber = seat[1];
0018 }
0019 
0020 function parseDeparture(res, line, year, compact) {
0021     var dep = line.match(/^(.+?) *([0-9]{2})\.([0-9]{2})\. +ab ([0-9]{2}:[0-9]{2})/);
0022     if (!dep)
0023         return false;
0024 
0025     res.reservationFor.departureStation.name = dep[1];
0026     res.reservationFor.departureTime = JsonLd.toDateTime(dep[2] + ' ' + dep[3] + ' ' + year + ' ' + dep[4], "dd MM yyyy hh:mm", "de");
0027     var idx = dep.index + dep[0].length;
0028     var platform = line.substr(idx).match(/^ {1,3}(.*?)(?=(  | IC|$))/);
0029     if (platform) {
0030         idx += platform.index + platform[0].length;
0031         res.reservationFor.departurePlatform = platform[1];
0032     }
0033     var trainId = line.substr(idx).match(compact ? / +([^,]*?)(?=(,|$))/ : / +(.*?)(?=(  |$))/);
0034     if (trainId) {
0035         idx += trainId.index + trainId[0].length
0036         res.reservationFor.trainNumber = trainId[1];
0037     }
0038     parseSeat(res, line.substr(idx));
0039     return true;
0040 }
0041 
0042 function parseArrival(res, line, year) {
0043     var arr = line.match(/^(.+?) *([0-9]{2})\.([0-9]{2})\. +an ([0-9]{2}:[0-9]{2})/);
0044     if (!arr)
0045         return false;
0046     res.reservationFor.arrivalStation.name = arr[1];
0047     res.reservationFor.arrivalTime = JsonLd.toDateTime(arr[2] + ' ' + arr[3] + ' ' + year + ' ' + arr[4], "dd MM yyyy hh:mm", "de");
0048     var idx = arr.index + arr[0].length;
0049     var platform = line.substr(idx).match(/^ {1,3}(.*?)(?=(  | IC| \d Fenster| \d Gang|$))/);
0050     if (platform) {
0051         idx += platform.index + platform[0].length;
0052         res.reservationFor.arrivalPlatform = platform[1];
0053     }
0054     parseSeat(res, line.substr(idx));
0055     return true;
0056 }
0057 
0058 function parseLegs(text, year, compact) {
0059     var reservations = new Array();
0060     var lines = text.split('\n');
0061     var offset = lines[0].match(/^ */);
0062     for (var i = 0; compact && i < lines.length; ++i)
0063         lines[i] = lines[i].substr(offset[0].length);
0064 
0065     for (var i = 0; i < lines.length;) {
0066         // stop when reaching the footer or the next itinerary header
0067         if (isHeaderOrFooter(lines[i]))
0068             return reservations;
0069 
0070         var res = JsonLd.newTrainReservation();
0071         while (i < lines.length && !isHeaderOrFooter(lines[i])) {
0072             if (parseDeparture(res, lines[i++], year, compact))
0073                 break;
0074         }
0075         while (i < lines.length && !isHeaderOrFooter(lines[i])) {
0076             if (parseArrival(res, lines[i], year)) {
0077                 ++i;
0078                 break;
0079             }
0080 
0081             // continuation of departure line
0082             var depStation = lines[i].match(/^(\S.*?)(?:  |\n|$)/)
0083             if (depStation)
0084                 res.reservationFor.departureStation.name = res.reservationFor.departureStation.name +  " " + depStation[1];
0085             parseSeat(res, lines[i]);
0086 
0087             ++i;
0088         }
0089         // handle continuations of the arrival line
0090         while (i < lines.length && !isHeaderOrFooter(lines[i])) {
0091             if (lines[i].match(/^\S.+? *[0-9]{2}\.[0-9]{2}\. +ab [0-9]{2}:[0-9]{2}/)) // next departure line
0092                 break;
0093 
0094             // continuation of arrival line
0095             var arrStation = lines[i].match(/^(\S.*?)(?:  |\n|$)/)
0096             if (arrStation)
0097                 res.reservationFor.arrivalStation.name = res.reservationFor.arrivalStation.name +  " " + arrStation[1];
0098             parseSeat(res, lines[i]);
0099 
0100             ++i;
0101         }
0102 
0103         if (res.reservationFor.arrivalStation != undefined) {
0104             reservations.push(res);
0105         } else {
0106             ++i;
0107         }
0108     }
0109 
0110     return reservations;
0111 }
0112 
0113 function parseText(text) { // used by unit tests
0114     return parseTicket(text, null);
0115 }
0116 
0117 function parseTicket(text, uic918ticket) {
0118     var reservations = new Array();
0119     var pos = 0;
0120     var returnResIndex = 0;
0121     while (true) {
0122         // find itinerary headers
0123         var header = text.substr(pos).match(/Ihre Reiseverbindung[\S ]+(Hin|Rück|Einfache )[fF]ahrt am [0-9]{2}.[0-9]{2}.([0-9]{4}).*\n/);
0124         if (!header)
0125             break;
0126         var idx = header.index + header[0].length;
0127         var year = header[2];
0128 
0129         // determine ticket type
0130         var domesticHeader = text.substr(pos + idx).match(/  Reservierung(?: \/ Hinweise)?\n/);
0131         var intlHeader = text.substr(pos + idx).match(/(Produkte\/Reservierung|Fahrt\/Reservierung).*\n/);
0132         if (domesticHeader) {
0133             idx += domesticHeader.index + domesticHeader[0].length;
0134             reservations = reservations.concat(parseLegs(text.substr(pos + idx), year, false));
0135         } else if (intlHeader) {
0136             idx += intlHeader.index + intlHeader[0].length;
0137             reservations = reservations.concat(parseLegs(text.substr(pos + idx), year, true));
0138         } else {
0139             break;
0140         }
0141 
0142         // for outward journeys we have station ids from the UIC 918-3 code
0143         if (uic918ticket && header[1] !== "Rück") {
0144             reservations[0].reservationFor.departureStation = JsonLd.apply(JsonLd.toJson(uic918ticket.outboundDepartureStation), reservations[0].reservationFor.departureStation);
0145             reservations[reservations.length - 1].reservationFor.arrivalStation = JsonLd.apply(JsonLd.toJson(uic918ticket.outboundArrivalStation), reservations[reservations.length - 1].reservationFor.arrivalStation);
0146             returnResIndex = reservations.length;
0147         } else {
0148             // propagate station ids from outward to return journey
0149             for (var i = returnResIndex; i < reservations.length; ++i) {
0150                 for (var j = 0; j < returnResIndex; ++j) {
0151                     if (reservations[i].reservationFor.departureStation.name === reservations[j].reservationFor.arrivalStation.name)
0152                         reservations[i].reservationFor.departureStation.identifier = reservations[j].reservationFor.arrivalStation.identifier;
0153                     if (reservations[i].reservationFor.arrivalStation.name === reservations[j].reservationFor.departureStation.name)
0154                         reservations[i].reservationFor.arrivalStation.identifier = reservations[j].reservationFor.departureStation.identifier;
0155                 }
0156             }
0157         }
0158 
0159         if (idx == 0)
0160             break;
0161         pos += idx;
0162     }
0163 
0164     // international tickets have the booking reference somewhere on the side, so we don't really know
0165     // where it is relative to the itinerary
0166     const bookingRef = text.match(/(?:Auftragsnummer|Auftrag \(NVS\)):\s*([A-Z0-9]{6,9}|\d{12})\n/);
0167     const price = text.match(/(?:Summe|Gesamtpreis) *(\d+,\d{2} ?€)/)[1];
0168     for (var i = 0; i < reservations.length; ++i) {
0169         if (bookingRef) {
0170             reservations[i].reservationNumber = bookingRef[1];
0171         }
0172 
0173         if (reservations[i].reservationFor.trainNumber && reservations[i].reservationFor.trainNumber.startsWith("Bus")) {
0174             reservations[i] = JsonLd.trainToBusReservation(reservations[i]);
0175             reservations[i].reservedTicket.ticketedSeat = undefined;
0176         }
0177         ExtractorEngine.extractPrice(price, reservations[i]);
0178     }
0179     return reservations;
0180 }
0181 
0182 function parseReservation(pdf) {
0183     var text = pdf.text;
0184     var reservations = Array();
0185     var idx = 0;
0186 
0187     while (true) {
0188         var dep = text.substr(idx).match(/  (\S.*\S) +(\d\d.\d\d.\d\d) +ab (\d\d:\d\d)  +(.*?)  +([A-Z].*\S)  +(.*)\n/);
0189         if (!dep)
0190             break;
0191         idx += dep.index + dep[0].length;
0192         var arr = text.substr(idx).match(/  (\S.*\S) +an (\d\d:\d\d)  +(.*?)  +(.*)\n/);
0193         if (!arr)
0194             break;
0195 
0196         var res = JsonLd.newTrainReservation();
0197         res.reservationFor.departureStation.name = dep[1];
0198         res.reservationFor.departureTime = JsonLd.toDateTime(dep[2] + dep[3], "dd.MM.yyhh:mm", "de");
0199         res.reservationFor.trainNumber = dep[5];
0200         res.reservationFor.departurePlatform = dep[4];
0201         res.reservationFor.arrivalStation.name = arr[1];
0202         res.reservationFor.arrivalTime = JsonLd.toDateTime(dep[2] + arr[2], "dd.MM.yyhh:mm", "de");
0203         res.reservationFor.arrivalPlatform = arr[3];
0204         reservations.push(res);
0205 
0206         seatText = dep[6] + " " + arr[4];
0207         var seat = seatText.match(/(\d)\. Klasse, Wg\. (\d+), Pl. (.*?),/);
0208         if (seat) {
0209             res.reservedTicket.ticketedSeat.seatingType = seat[1];
0210             res.reservedTicket.ticketedSeat.seatSection = seat[2];
0211             res.reservedTicket.ticketedSeat.seatNumber = seat[3];
0212         }
0213 
0214         idx += arr.index + arr[0].length;
0215     }
0216 
0217     ExtractorEngine.extractPrice(text, reservations);
0218     return reservations;
0219 }
0220 
0221 function applyUic9183ToReservation(res, uicNode)
0222 {
0223     const uicCode = uicNode.content;
0224     if (!res.reservationNumber || res.reservationNumber == uicCode.pnr)
0225         res.reservationNumber = uicCode.pnr;
0226     else
0227         res.reservedTicket.ticketNumber = uicCode.pnr;
0228     res.reservationFor.provider = JsonLd.toJson(uicCode.issuer);
0229     if (uicNode.result.length > 0 && uicNode.result[0].programMembershipUsed)
0230         res.programMembershipUsed = uicNode.result[0].programMembershipUsed;
0231     const bl = uicCode.block('0080BL');
0232     if (bl) {
0233         const sb = bl.findSubBlock('009');
0234         if (sb) {
0235             const bc = sb.content.match(/\d+-\d+-(.*)/)[1];
0236             switch (bc) {
0237                 case "49":
0238                     res.programMembershipUsed.programName = "BahnCard 25";
0239                     break;
0240                 case "19":
0241                 case "78":
0242                     res.programMembershipUsed.programName = "BahnCard 50";
0243                     break;
0244             }
0245         }
0246     }
0247     res.reservedTicket.name = uicCode.name
0248     if (res.reservedTicket.ticketedSeat)
0249         res.reservedTicket.ticketedSeat.seatingType = uicCode.seatingType;
0250     res.underName = JsonLd.toJson(uicCode.person);
0251 }
0252 
0253 function parsePdf(pdf, node, triggerNode) {
0254     var page = pdf.pages[triggerNode.location];
0255     var uic918ticket = triggerNode.mimeType == "internal/uic9183" ? triggerNode.content : null;
0256 
0257     var reservations = parseTicket(page.text, uic918ticket);
0258     for (var i = 0; i < reservations.length; ++i) {
0259         reservations[i].reservedTicket.ticketToken = "aztecbin:" + ByteArray.toBase64(triggerNode.content.rawData);
0260         if (triggerNode.mimeType == "internal/uic9183") {
0261             applyUic9183ToReservation(reservations[i], triggerNode);
0262         } else if (triggerNode.mimeType == "internal/vdv") {
0263             reservations[i].reservationFor.provider.identifier = "vdv:" + triggerNode.content.operatorId;
0264             if (reservations[i].reservedTicket.ticketedSeat) {
0265                 const c = triggerNode.content.seatingClass;
0266                 reservations[i].reservedTicket.ticketedSeat.seatingType = (c == 1 || c == 3) ? '1' : '2';
0267             }
0268             reservations[i].underName = JsonLd.toJson(triggerNode.content.person);
0269         }
0270     }
0271 
0272     return reservations;
0273 }
0274 
0275 function parseCancellation(html) {
0276     var title = html.eval('//title')[0];
0277     var pnr = title.content.match(/Stornierungsbestätigung \(Auftrag (.*)\)/);
0278     if (!pnr)
0279         return null;
0280     var res = JsonLd.newTrainReservation();
0281     res.reservationNumber = pnr[1];
0282     res.reservationStatus = "ReservationCancelled";
0283     res.reservationFor = 1;
0284     return res;
0285 }
0286 
0287 function parseUic9183(code, node) {
0288     // Bahncard code
0289     if (code.ticketLayout && code.ticketLayout.type == "RCT2" && code.ticketLayout.text(0, 12, 40, 1).match(/BAHNCARD/i)) {
0290         var bc = JsonLd.newObject("ProgramMembership");
0291         bc.programName = code.ticketLayout.text(1, 12, 40, 1);
0292         bc.membershipNumber = code.ticketLayout.text(14, 11, 16, 1);
0293         bc.member = JsonLd.toJson(code.person);
0294         bc.token = 'aztecbin:' + ByteArray.toBase64(code.rawData);
0295         bc.validFrom = JsonLd.readQDateTime(code, 'validFrom');
0296         bc.validUntil = JsonLd.readQDateTime(code, 'validUntil');
0297         return bc.programName != undefined ? bc : undefined;
0298     }
0299 
0300     // domestic ticket code
0301     const bl = code.block('0080BL');
0302     if (bl && code.outboundDepartureStation.name && code.outboundArrivalStation.name) {
0303         let res = JsonLd.newTrainReservation();
0304         res.reservedTicket = node.result[0];
0305         applyUic9183ToReservation(res, node);
0306         res.reservationFor.departureDay = JsonLd.toDateTime(bl.findSubBlock('031').content, 'dd.MM.yyyy', 'de');
0307         res.reservationFor.departureStation = JsonLd.toJson(code.outboundDepartureStation);
0308         res.reservationFor.arrivalStation = JsonLd.toJson(code.outboundArrivalStation);
0309 
0310         if (!bl.findSubBlock('017')) {
0311             return res;
0312         }
0313 
0314         let ret = JsonLd.newTrainReservation();
0315         ret.reservedTicket = node.result[0];
0316         applyUic9183ToReservation(ret, node);
0317         ret.reservationFor.departureDay = JsonLd.toDateTime(bl.findSubBlock('032').content, 'dd.MM.yyyy', 'de');
0318         ret.reservationFor.departureStation = JsonLd.toJson(code.returnDepartureStation);
0319         ret.reservationFor.arrivalStation = JsonLd.toJson(code.returnArrivalStation);
0320         return [res, ret];
0321     }
0322 }
0323 
0324 function parseEvent(event) {
0325     let res = JsonLd.newTrainReservation();
0326     const names = event.summary.match(/(.*) -> (.*)/);
0327     res.reservationFor.departureStation.name = names[1];
0328     res.reservationFor.departureTime = JsonLd.readQDateTime(event, 'dtStart');
0329     res.reservationFor.arrivalStation.name = names[2];
0330     res.reservationFor.arrivalTime = JsonLd.readQDateTime(event, 'dtEnd');
0331 
0332     // search for more details in the description
0333     let reservations = [];
0334     let idx = 0;
0335     while (true) {
0336         const trip = event.description.substr(idx).match(/(\d{2}:\d{2}) (.*?)(?:- (?:Gleis|platform|voie|Vía|Spor|Kolej|binario|Peron) (.*?))?(?: \((.*\d+)\))?\n.* (\d{2}:\d{2}) (.*?)(?:\n| - (?:Gleis|platform|voie|Vía|Spor|Kolej|binario|Peron) (.*)\n)/);
0337         if (!trip) {
0338             break;
0339         }
0340         idx += trip.index + trip[0].length;
0341 
0342         let res = JsonLd.newTrainReservation();
0343         const date = JsonLd.readQDateTime(event, 'dtStart')['@value'].substr(0, 10);
0344         res.reservationFor.departureStation.name = trip[2];
0345         res.reservationFor.departureTime = JsonLd.toDateTime(date + trip[1], 'yyyy-MM-ddhh:mm', 'de');
0346         res.reservationFor.departurePlatform = trip[3];
0347         res.reservationFor.trainName = trip[4];
0348         res.reservationFor.arrivalStation.name = trip[6];
0349         res.reservationFor.arrivalTime = JsonLd.toDateTime(date + trip[5], 'yyyy-MM-ddhh:mm', 'de');
0350         res.reservationFor.arrivalPlatform = trip[7];
0351 
0352         if (trip[4] && trip[4].match(/^Bus[ \d]/)) {
0353             res = JsonLd.trainToBusReservation(res);
0354         }
0355 
0356         reservations.push(res);
0357     }
0358     // recover full timezones for the begin/end
0359     if (reservations.length > 0) {
0360         reservations[0].reservationFor.departureTime = res.reservationFor.departureTime;
0361         reservations[reservations.length - 1].reservationFor.arrivalTime = res.reservationFor.arrivalTime;
0362         return reservations;
0363     }
0364 
0365     return res;
0366 }
0367 
0368 function parseDBRegioBusUic(uic, node)
0369 {
0370     let ticket = node.result[0];
0371     if (uic.ticketLayout.type != 'PLAI' || ticket.name)
0372         return;
0373 
0374     ticket.name = uic.ticketLayout.firstField.text;
0375     for (let f = uic.ticketLayout.firstField; f && !f.isNull; f = f.next) {
0376         const validFrom = f.text.match(/Erster Gültigkeitstag: (.*)/);
0377         if (validFrom) {
0378             ticket.validFrom = JsonLd.toDateTime(validFrom[1], 'dd.MM.yyyy', 'de');
0379         }
0380         const name = f.text.match(/Name Fahrtberechtigter: (.*)/);
0381         if (name) {
0382             ticket.underName = JsonLd.newObject('Person');
0383             ticket.underName.name = name[1];
0384         }
0385     }
0386     if (ticket.name.includes("Deutschland-Ticket") && !ticket.validUntil) {
0387         ticket.validUntil = JsonLd.clone(ticket.validFrom)
0388         ticket.validUntil.setMonth(ticket.validUntil.getMonth() + 1)
0389         ticket.validUntil.setHours(3)
0390     }
0391     return ticket;
0392 }