import { compress, decompress } from './smaz.js'; import { base64EncArr, base64DecToArr } from './base64.js'; const maxSize = 30000; const buffer = new Uint8Array(maxSize); let pos = 0; let decBuffer; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); const legTypes = [ "T", "G@F", "TF", "D", "W" ]; const addToBuffer = content => { buffer.set(content, pos); pos += content.length * (content.BYTES_PER_ELEMENT || 1); }; const encodeStringCompressed = content => { const encoded = textEncoder.encode(content); const compressed = compress(encoded); addToBuffer([compressed.length]); addToBuffer(compressed); }; const encodeString = content => { const encoded = textEncoder.encode(content); addToBuffer([encoded.length]); addToBuffer(encoded); }; const encodeTrainsearchPlace = data => { addToBuffer([data.placeType]); if (data.placeType === 1) { // Station encodeU32(data.id); } else if (data.placeType === 2) { // Address encodeStringCompressed(data.name); encodeI32(data.x); encodeI32(data.y); } else if (data.placeType === 4) { // Poi encodeU32(data.id); encodeStringCompressed(data.name); encodeI32(data.x); encodeI32(data.y); } else { throw "unknown place type"; } }; const encodeI32 = data => { const buffer = new ArrayBuffer(4); new DataView(buffer).setInt32(0, data, true /* littleEndian */); addToBuffer(new Uint8Array(buffer)); }; const encodeU32 = data => { const buffer = new ArrayBuffer(4); new DataView(buffer).setUint32(0, data, true /* littleEndian */); addToBuffer(new Uint8Array(buffer)); }; const encodeU16 = data => { const buffer = new ArrayBuffer(2); new DataView(buffer).setUint16(0, data, true /* littleEndian */); addToBuffer(new Uint8Array(buffer)); }; const encodeTrainsearch = data => { pos = 0; addToBuffer([0]); // version encodeTrainsearchPlace(data.legs[0].from); for (let leg of data.legs) { addToBuffer([legTypes.indexOf(leg.legType)]); encodeTrainsearchPlace(leg.to); let departureMins = Math.floor(leg.departure / 60000); let arrivalMins = Math.floor(leg.arrival / 60000); encodeU32(departureMins); encodeU16(arrivalMins - departureMins); encodeString(leg.line); addToBuffer([leg.field7]); } return base64EncArr(buffer.slice(0, pos)); }; const readByte = () => { if (decBuffer.length < 1) throw "short read"; const ret = decBuffer[0]; decBuffer = decBuffer.slice(1); return ret; }; const decodeI32 = () => { const ret = new DataView(decBuffer.buffer).getInt32(0, true /* littleEndian */); decBuffer = decBuffer.slice(4); return ret; }; const decodeU32 = () => { const ret = new DataView(decBuffer.buffer).getUint32(0, true /* littleEndian */); decBuffer = decBuffer.slice(4); return ret; }; const decodeU16 = () => { const ret = new DataView(decBuffer.buffer).getUint16(0, true /* littleEndian */); decBuffer = decBuffer.slice(2); return ret; }; const decodeString = () => { const len = readByte(); if (decBuffer.length < len) throw "short read"; const bytes = decBuffer.slice(0, len); decBuffer = decBuffer.slice(len); return textDecoder.decode(bytes); }; const decodeStringCompressed = () => { const len = readByte(); if (decBuffer.length < len) throw "short read"; const bytes = decBuffer.slice(0, len); decBuffer = decBuffer.slice(len); const decompressed = decompress(bytes); return textDecoder.decode(decompressed); }; const decodeTrainsearchPlace = () => { const placeType = readByte(); if (placeType === 1) { // Station const id = decodeU32(); return { placeType, id }; } else if (placeType === 2) { // Address const name = decodeStringCompressed(); const x = decodeI32(); const y = decodeI32(); return { placeType, name, x, y }; } else if (placeType === 4) { // Poi const id = decodeU32(); const name = decodeStringCompressed(); const x = decodeI32(); const y = decodeI32(); return { placeType, id, name, x, y }; } else { throw "unknown place type"; } }; const decodeTrainsearch = data => { const length = base64DecToArr(data, buffer); decBuffer = buffer.slice(0, length); const version = readByte(); if (version !== 0) throw "unknown trainsearch token version"; const legs = []; let lastPlace = decodeTrainsearchPlace(); while (decBuffer.length > 0) { const legType = legTypes[readByte()]; const to = decodeTrainsearchPlace(); const departureMin = decodeU32(); const diff = decodeU16(); const departure = new Date(departureMin * 60000); const arrival = new Date((departureMin + diff) * 60000); const line = decodeString(); const field7 = readByte(); legs.push({ legType, from: lastPlace, to, departure, arrival, line, field7 }); lastPlace = to; } return { legs }; }; const decodeHafasPlace = data => { let values = {}; for (let pair of data.split("@")) { if (pair == "") continue; const [key, value] = pair.split("="); values[key] = value; } const placeType = parseInt(values["A"]); if (placeType === 1) { // Station const id = parseInt(values["L"]); return { placeType, id }; } else if (placeType === 2) { // Address const name = values["O"]; const x = parseInt(values["X"]); const y = parseInt(values["Y"]); return { placeType, name, x, y }; } else if (placeType === 4) { // Poi const id = parseInt(values["L"]); const name = values["O"]; const x = parseInt(values["X"]); const y = parseInt(values["Y"]); return { placeType, id, name, x, y }; } else { throw "unknown place type"; } }; const decodeHafasDate = data => { return new Date(`${data.slice(0, 4)}-${data.slice(4, 6)}-${data.slice(6, 8)}T${data.slice(8, 10)}:${data.slice(10, 12)}Z`); }; const decodeHafas = data => { if (data.startsWith("¶HKI¶")) { data = data.slice(5); } data = data.split("¶GP¶")[0]; const legs = []; for (let leg of data.split("§")) { const parts = leg.split("$"); const legType = parts[0]; const from = decodeHafasPlace(parts[1]); const to = decodeHafasPlace(parts[2]); const departure = decodeHafasDate(parts[3]); const arrival = decodeHafasDate(parts[4]); const line = parts[5].replaceAll(" ", ""); //if (parts[6] !== "") throw `unexpected field 6 with content ${parts[6]}`; const field7 = parseInt(parts[7]); for (let pos = 8; pos < parts.length; pos += 1) { //if (parts[pos] !== "") throw `unexpected field ${pos} with content ${parts[pos]}`; } legs.push({ legType, from, to, departure, arrival, line, field7 }); } return { legs }; }; const encodeHafasDate = data => { const iso = data.toISOString(); let out = ""; out += iso.slice(0, 4); out += iso.slice(5, 7); out += iso.slice(8, 10); out += iso.slice(11, 13); out += iso.slice(14, 16); return out; }; const encodeHafasPlace = data => { const parts = {}; parts["A"] = data.placeType.toString(); if (data.placeType === 1) { // Station parts["L"] = data.id.toString(); } else if (data.placeType === 2) { // Address parts["O"] = data.name; parts["X"] = data.x.toString(); parts["Y"] = data.y.toString(); } else if (data.placeType === 4) { // Poi parts["L"] = data.id.toString(); parts["O"] = data.name; parts["X"] = data.x.toString(); parts["Y"] = data.y.toString(); } else { throw "unknown place type"; } return Object.entries(parts).map(([key, value]) => `${key}=${value}@`).join(""); }; const encodeHafas = data => { const legs = []; for (let leg of data.legs) { const parts = []; parts.push(leg.legType); parts.push(encodeHafasPlace(leg.from)); parts.push(encodeHafasPlace(leg.to)); parts.push(encodeHafasDate(leg.departure)); parts.push(encodeHafasDate(leg.arrival)); parts.push(leg.line); parts.push(""); parts.push(leg.field7.toString()); parts.push(""); parts.push(""); parts.push(""); legs.push(parts.join("$")); } return `¶HKI¶${legs.join("§")}`; }; export const hafasToTrainsearch = data => { return encodeTrainsearch(decodeHafas(data)); }; export const trainsearchToHafas = data => { return encodeHafas(decodeTrainsearch(data)); };