function log(msg) { console.log("[Bahn Insight] " + msg); } function getMediaURL(path) { path = "media/" + path; return chrome.runtime.getURL(path) || browser.runtime.getURL(path); } /** * Return train name in format "PRODUCT TRAIN_NUMER" * i.e. "ICE 112", "RE 12734" * Sometimes train name is in format "PRODUCT LINE_NUMER (TRAIN_NUMER)" * i.e. "STB 12 (62371)" * Sometimes 'trains' are not trains or we can't find an unique id for them * i.e. busses, ferrys, trams * they will also be cleaned up and returned as undefined, because we can't link to them correctly */ function bahnParseTrainName(dirty_train_name) { var name_list = dirty_train_name.trim().replace(/ +/g, ' ').split(" "); // Train name in format "STB 12 (23561)" if(name_list.length == 3 && name_list[2].charAt(0) == '(' && name_list[2].charAt(name_list[2].length-1) == ')') { return name_list[0] + " " + name_list[2].substring(1, name_list[2].length-1); } // Exclude linking to specific products if(["bus", "fäh", "str"].indexOf(name_list[0].toLowerCase()) !== -1) { return undefined; } return dirty_train_name; } /** * Returns date string in format YYYYMMDDHHMM * * @param datetime Date object */ function datetimeToYYYYMMDDHHMM(datetime) { return datetime.getFullYear().toString().padStart(4, '0') + "" + (datetime.getMonth()+1).toString().padStart(2, '0') + "" + datetime.getDate().toString().padStart(2, '0') + "" + datetime.getHours().toString().padStart(2, '0') + "" + datetime.getMinutes().toString().padStart(2, '0') } /** * Returns HTML a as DOM object * * @param href URI as string */ function domCreateLink(href) { var link = document.createElement("a"); link.setAttribute("href", href); return link; } /** * Returns HTML img as DOM object * * @param src Image URI as string */ function domCreateImage(src) { var image = document.createElement("img"); image.setAttribute("src", src); return image; } /** * Returns HTML br as DOM object */ function domCreateLinebreak() { var linebreak = document.createElement("br"); return linebreak; } /** * Returns HTML span as DOM object */ function domCreateSpan() { var span = document.createElement("span"); return span; } /** * Returns an 'image button' like construct as DOM object * * @param href URI as string * @param image_src Image URI as string */ function domCreateButton(href, image_src) { var link = domCreateLink(href); link.setAttribute("target", "_blank"); var image = domCreateImage(image_src); image.setAttribute("style", "height: 16px; vertical-align:middle;"); link.append(image); return link; } /** * Returns an 'image buttom' to specifically link to marudor.de * * @param path URL path part after https://marudor.de */ function domCreateButtonMarudor(path) { var button = domCreateButton("https://marudor.de" + path, getMediaURL("marudor.svg")); button.setAttribute("title", "marudor.de"); return button; } /** * Returns an 'image buttom' to specifically link to dbf.finalrewind.org * * @param path URL path part after https://dbf.finalrewind.org */ function domCreateButtonDbf(path) { var button = domCreateButton("https://dbf.finalrewind.org" + path, getMediaURL("dbf.png")); button.setAttribute("title", "dbf.finalrewind.org"); return button; } /** * Returns a 'bahn-insight' element as DOM object * * Used to determine if this is an object set by this extension itself */ function domCreateBahnInsightField() { var span = domCreateSpan(); span.setAttribute("class", "bahn-insight"); return span; } var connection_result_observer = new MutationObserver((mutations) => { log("change detected") mutations.forEach((mutation) => { if (mutation.type === 'childList') { var target = mutation.target; if (target.tagName === 'TD') { log("change is interesting"); var timetable = target.querySelector("td div.detailContainer table.result tbody"); // Just break if there are elements injected by us if(timetable.querySelectorAll(".bahn-insight").length != 0) { log("links already injected") return; } /* * COLLECT DATA */ var data = []; var relations = timetable.querySelectorAll("tr.first"); relations.forEach((relation, i) => { var relationend = relation.nextElementSibling; data[i] = {}; data[i]["from"] = {} data[i]["to"] = {} // Depature station name var relation_from = relation.querySelector("td.station"); data[i]["from"]["station"] = relation_from.innerText; // Arrival station name var relation_to = relationend.querySelector("td.station"); data[i]["to"]["station"] = relation_to.innerText; // Departure time & current estimation var relation_departure = relation.querySelector("td.time"); var relation_departure_list = relation_departure.firstChild.textContent.trim().split(" "); data[i]["from"]["time"] = relation_departure_list[1]; data[i]["from"]["time_current"] = null; var relation_departure_current = relation_departure.querySelector("span.delay, span.delayOnTime"); if(relation_departure_current != null) { data[i]["from"]["time_current"] = relation_departure_current.innerText.trim(); } // Arrival time & current estimation var relation_arrival = relationend.querySelector("td.time"); var relation_arrival_list = relation_arrival.firstChild.textContent.trim().split(" "); data[i]["to"]["time"] = relation_arrival_list[1]; data[i]["to"]["time_current"] = null; var relation_arrival_current = relation_arrival.querySelector("span.delay, span.delayOnTime"); if(relation_arrival_current != null) { data[i]["to"]["time_current"] = relation_arrival_current.innerText.trim(); } // Departure platform var relation_platform = relation.querySelector("td.platform"); data[i]["from"]["platform"] = relation_platform.innerText; // Arrival platform var relation_to_platform = relationend.querySelector("td.platform"); data[i]["to"]["platform"] = relation_to_platform.innerText; // Travel products var relation_products = relation.querySelector("td.products"); data[i]["products"] = []; // Fetch all products for this travel var relation_trains = relation_products.querySelectorAll("span a"); relation_trains.forEach((train, j) => { data[i]["products"][j] = {}; // Prodcut details data[i]["products"][j]["type"] = null; data[i]["products"][j]["train_number"] = null; data[i]["products"][j]["train_name"] = null; data[i]["products"][j]["line_name"] = null; var name_list = train.innerText.trim().replace(/ +/g, ' ').split(" "); // Product name in format "STB 12 (23561)" if(name_list.length == 3 && name_list[2].charAt(0) == '(' && name_list[2].charAt(name_list[2].length-1) == ')') { data[i]["products"][j]["type"] = name_list[0]; data[i]["products"][j]["train_number"] = name_list[2].substring(1, name_list[2].length-1) data[i]["products"][j]["line_name"] = name_list[0] + " " + name_list[1]; } // Product name in format "ICE 234" else { data[i]["products"][j]["type"] = name_list[0]; data[i]["products"][j]["train_number"] = name_list[1]; } data[i]["products"][j]["train_name"] = data[i]["products"][j]["type"] + " " + data[i]["products"][j]["train_number"]; }); // Travel information var relation_items = relation.querySelectorAll("td"); var relation_info = relation_items[relation_items.length-1]; data[i]["info"] = relation_info.innerText; }); log("data collected"); console.log(data); /* * INJECT CUSTOM UI */ var relations = timetable.querySelectorAll("tr.first"); relations.forEach((relation, i) => { var relationend = relation.nextElementSibling; var products = relation.querySelectorAll("td.products span a"); products.forEach((product, j) => { // Field for 'bahn-insight' stuff var bahn_insight_field = domCreateBahnInsightField(); product.after(bahn_insight_field); var departure_time = new Date(Date.parse(connection_result_date + " " + data[i]["from"]["time"])); // Button linking to marudor.de var marudor_button = domCreateButtonMarudor("/details/" + data[i]["products"][j]["train_name"] + "/" + departure_time.getTime()); bahn_insight_field.appendChild(marudor_button); var dbf_button = domCreateButtonDbf("/_wr/" + data[i]["products"][j]["train_number"] + "/" + datetimeToYYYYMMDDHHMM(departure_time)); bahn_insight_field.appendChild(dbf_button); // Move linebreaks from link inner, after our 'bahn-insight' field var linebreaks = product.querySelectorAll("br"); if(linebreaks.length != 0) { linebreaks.forEach((linebreak) => { product.removeChild(linebreak); }); bahn_insight_field.after(domCreateLinebreak()); } }); // Linking to station var from = relation.querySelector("td.station"); var bahn_insight_field = domCreateBahnInsightField(); from.appendChild(bahn_insight_field); var marudor_button = domCreateButtonMarudor("/" + data[i]["from"]["station"]); bahn_insight_field.appendChild(marudor_button); var dbf_button = domCreateButtonDbf("/" + data[i]["from"]["station"]); bahn_insight_field.appendChild(dbf_button); // Linking to station var to = relationend.querySelector("td.station"); var bahn_insight_field = domCreateBahnInsightField(); to.appendChild(bahn_insight_field); var marudor_button = domCreateButtonMarudor("/" + data[i]["to"]["station"]); bahn_insight_field.appendChild(marudor_button); var dbf_button = domCreateButtonDbf("/" + data[i]["to"]["station"]); bahn_insight_field.appendChild(dbf_button); }); log("ui injected"); } } }); }); var target = document.getElementById('resultsOverview'); if(typeof target !== 'undefined') { var connection_result_date = document.querySelector("html body div div div.resultContentHolder form div h2 span").innerText.replace(/(\d{2})\.(\d{2})\.(\d{2})/,'20$3-$2-$1'); connection_result_observer.observe(target, { subtree: true, childList: true }); log("observation started") }