UNPKG

gtfs-to-html

Version:

Build human readable transit timetables as HTML, PDF or CSV from GTFS

2,750 lines 86.8 kB
#!/usr/bin/env node
var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/bin/gtfs-to-html.ts
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import PrettyError from "pretty-error";

// src/lib/file-utils.ts
import { dirname, join, resolve } from "path";
import cssEscape from "css.escape";
import { createWriteStream } from "fs";
import { fileURLToPath } from "url";
import {
  access,
  cp,
  copyFile,
  mkdir,
  readdir,
  readFile,
  rm
} from "fs/promises";
import { homedir } from "os";
import * as _ from "lodash-es";
import { uniqBy as uniqBy2 } from "lodash-es";
import archiver from "archiver";
import beautify from "js-beautify";
import sanitizeHtml from "sanitize-html";
import { renderFile } from "pug";
import puppeteer from "puppeteer";
import sanitize from "sanitize-filename";
import { marked } from "marked";

// src/lib/formatters.ts
import {
  clone,
  find as find2,
  first as first2,
  groupBy as groupBy2,
  last as last2,
  omit,
  sortBy as sortBy2,
  zipObject
} from "lodash-es";
import moment3 from "moment";

// src/lib/time-utils.ts
import moment from "moment";
function fromGTFSTime(timeString) {
  const duration = moment.duration(timeString);
  return moment({
    hour: duration.hours(),
    minute: duration.minutes(),
    second: duration.seconds()
  });
}
function toGTFSTime(time) {
  return time.format("HH:mm:ss");
}
function calendarToCalendarCode(calendar) {
  if (Object.values(calendar).every((value) => value === null)) {
    return "";
  }
  return `${calendar.monday ?? "0"}${calendar.tuesday ?? "0"}${calendar.wednesday ?? "0"}${calendar.thursday ?? "0"}${calendar.friday ?? "0"}${calendar.saturday ?? "0"}${calendar.sunday ?? "0"}`;
}
function calendarCodeToCalendar(code) {
  const days2 = [
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday",
    "sunday"
  ];
  const calendar = {};
  for (const [index, day] of days2.entries()) {
    calendar[day] = code[index];
  }
  return calendar;
}
function calendarToDateList(calendar, startDate, endDate) {
  if (!startDate || !endDate) {
    return [];
  }
  const activeWeekdays = [
    calendar.monday === 1 ? 1 : null,
    calendar.tuesday === 1 ? 2 : null,
    calendar.wednesday === 1 ? 3 : null,
    calendar.thursday === 1 ? 4 : null,
    calendar.friday === 1 ? 5 : null,
    calendar.saturday === 1 ? 6 : null,
    calendar.sunday === 1 ? 7 : null
  ].filter((weekday) => weekday !== null);
  if (activeWeekdays.length === 0) {
    return [];
  }
  const activeWeekdaySet = new Set(activeWeekdays);
  const dates = /* @__PURE__ */ new Set();
  const date = moment(startDate.toString(), "YYYYMMDD");
  const endDateMoment = moment(endDate.toString(), "YYYYMMDD");
  while (date.isSameOrBefore(endDateMoment)) {
    const isoWeekday = date.isoWeekday();
    if (activeWeekdaySet.has(isoWeekday)) {
      dates.add(parseInt(date.format("YYYYMMDD"), 10));
    }
    date.add(1, "day");
  }
  return Array.from(dates);
}
function secondsAfterMidnight(timeString) {
  return moment.duration(timeString).asSeconds();
}
function minutesAfterMidnight(timeString) {
  return moment.duration(timeString).asMinutes();
}
function updateTimeByOffset(timeString, offsetSeconds) {
  const newTime = fromGTFSTime(timeString);
  return toGTFSTime(newTime.add(offsetSeconds, "seconds"));
}

// src/lib/utils.ts
import {
  cloneDeep,
  compact,
  countBy,
  difference,
  entries,
  every as every2,
  find,
  findLast,
  first,
  flatMap,
  flow,
  groupBy,
  head,
  last,
  maxBy,
  orderBy,
  partialRight,
  reduce,
  size,
  some,
  sortBy,
  uniq,
  uniqBy,
  zip
} from "lodash-es";
import {
  getCalendarDates,
  getTrips,
  getTimetableNotesReferences,
  getTimetableNotes,
  getRoutes,
  getCalendars,
  getTimetableStopOrders,
  getStops,
  getStopAttributes,
  getStoptimes,
  getFrequencies,
  getTimetables,
  getTimetablePages,
  getAgencies as getAgencies2,
  openDb
} from "gtfs";
import { stringify } from "csv-stringify";
import moment2 from "moment";
import sqlString from "sqlstring";
import toposort from "toposort";

// src/lib/geojson-utils.ts
import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
import simplify from "@turf/simplify";
import { featureCollection, round } from "@turf/helpers";

// src/lib/log-utils.ts
import { clearLine, cursorTo } from "readline";
import { noop } from "lodash-es";
import * as colors from "yoctocolors";
import { getAgencies, getFeedInfo } from "gtfs";
import Table from "cli-table";
function generateLogText(outputStats, config) {
  const feedInfo = getFeedInfo();
  const agencies = getAgencies();
  const feedVersion = feedInfo.length > 0 && feedInfo[0].feed_version ? feedInfo[0].feed_version : "Unknown";
  const logText = [
    `Agencies: ${agencies.map((agency) => agency.agency_name).join(", ")}`,
    `Feed Version: ${feedVersion}`,
    `GTFS-to-HTML Version: ${config.gtfsToHtmlVersion}`,
    `Date Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
    `Timetable Page Count: ${outputStats.timetablePages}`,
    `Timetable Count: ${outputStats.timetables}`,
    `Calendar Service ID Count: ${outputStats.calendars}`,
    `Route Count: ${outputStats.routes}`,
    `Trip Count: ${outputStats.trips}`,
    `Stop Count: ${outputStats.stops}`
  ];
  for (const agency of config.agencies) {
    if (agency.url) {
      logText.push(`Source: ${agency.url}`);
    } else if (agency.path) {
      logText.push(`Source: ${agency.path}`);
    }
  }
  if (outputStats.warnings.length > 0) {
    logText.push("", "Warnings:", ...outputStats.warnings);
  }
  return logText.join("\n");
}
function log(config) {
  if (config.verbose === false) {
    return noop;
  }
  if (config.logFunction) {
    return config.logFunction;
  }
  return (text, overwrite) => {
    if (overwrite === true && process.stdout.isTTY) {
      clearLine(process.stdout, 0);
      cursorTo(process.stdout, 0);
    } else {
      process.stdout.write("\n");
    }
    process.stdout.write(text);
  };
}
function logWarning(config) {
  if (config.logFunction) {
    return config.logFunction;
  }
  return (text) => {
    process.stdout.write(`
${formatWarning(text)}
`);
  };
}
function logError(config) {
  if (config.logFunction) {
    return config.logFunction;
  }
  return (text) => {
    process.stdout.write(`
${formatError(text)}
`);
  };
}
function formatWarning(text) {
  const warningMessage = `${colors.underline("Warning")}: ${text}`;
  return colors.yellow(warningMessage);
}
function formatError(error) {
  const messageText = error instanceof Error ? error.message : error;
  const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
    "Error: ",
    ""
  )}`;
  return colors.red(errorMessage);
}
function logStats(config) {
  if (config.logFunction) {
    return noop;
  }
  return (stats) => {
    const table = new Table({
      colWidths: [40, 20],
      head: ["Item", "Count"]
    });
    table.push(
      ["\u{1F4C4} Timetable Pages", stats.timetablePages],
      ["\u{1F551} Timetables", stats.timetables],
      ["\u{1F4C5} Calendar Service IDs", stats.calendars],
      ["\u{1F504} Routes", stats.routes],
      ["\u{1F68D} Trips", stats.trips],
      ["\u{1F6D1} Stops", stats.stops],
      ["\u26D4\uFE0F Warnings", stats.warnings.length]
    );
    log(config)(table.toString());
  };
}
var generateProgressBarString = (barTotal, barProgress, size2 = 40) => {
  const line = "-";
  const slider = "=";
  if (!barTotal) {
    throw new Error("Total value is either not provided or invalid");
  }
  if (!barProgress && barProgress !== 0) {
    throw new Error("Current value is either not provided or invalid");
  }
  if (isNaN(barTotal)) {
    throw new Error("Total value is not an integer");
  }
  if (isNaN(barProgress)) {
    throw new Error("Current value is not an integer");
  }
  if (isNaN(size2)) {
    throw new Error("Size is not an integer");
  }
  if (barProgress > barTotal) {
    return slider.repeat(size2 + 2);
  }
  const percentage = barProgress / barTotal;
  const progress = Math.round(size2 * percentage);
  const emptyProgress = size2 - progress;
  const progressText = slider.repeat(progress);
  const emptyProgressText = line.repeat(emptyProgress);
  return progressText + emptyProgressText;
};
function progressBar(formatString, barTotal, config) {
  let barProgress = 0;
  if (config.verbose === false) {
    return {
      increment: noop,
      interrupt: noop
    };
  }
  if (barTotal === 0) {
    return null;
  }
  const renderProgressString = () => formatString.replace("{value}", barProgress).replace("{total}", barTotal).replace("{bar}", generateProgressBarString(barTotal, barProgress));
  log(config)(renderProgressString(), true);
  return {
    interrupt(text) {
      logWarning(config)(text);
      log(config)("");
    },
    increment() {
      barProgress += 1;
      log(config)(renderProgressString(), true);
    }
  };
}

// src/lib/geojson-utils.ts
var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
var truncateGeoJSONDecimals = (geojson, config) => {
  for (const feature of geojson.features) {
    if (feature.geometry.coordinates) {
      if (feature.geometry.type.toLowerCase() === "point") {
        feature.geometry.coordinates = feature.geometry.coordinates.map(
          (number) => round(number, config.coordinatePrecision)
        );
      } else if (feature.geometry.type.toLowerCase() === "linestring") {
        feature.geometry.coordinates = feature.geometry.coordinates.map(
          (coordinate) => coordinate.map(
            (number) => round(number, config.coordinatePrecision)
          )
        );
      } else if (feature.geometry.type.toLowerCase() === "multilinestring") {
        feature.geometry.coordinates = feature.geometry.coordinates.map(
          (linestring) => linestring.map(
            (coordinate) => coordinate.map(
              (number) => round(number, config.coordinatePrecision)
            )
          )
        );
      }
    }
  }
  return geojson;
};
function getTimetableGeoJSON(timetable, config) {
  const shapesGeojsons = timetable.route_ids.map(
    (routeId) => getShapesAsGeoJSON({
      route_id: routeId,
      direction_id: timetable.direction_id,
      trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
    })
  );
  const stopsGeojsons = timetable.route_ids.map(
    (routeId) => getStopsAsGeoJSON({
      route_id: routeId,
      direction_id: timetable.direction_id,
      trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
    })
  );
  const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
  let simplifiedGeojson;
  try {
    simplifiedGeojson = simplify(geojson, {
      tolerance: 1 / 10 ** config.coordinatePrecision,
      highQuality: true
    });
  } catch {
    timetable.warnings.push(
      `Timetable ${timetable.timetable_id} - Unable to simplify geojson`
    );
    simplifiedGeojson = geojson;
  }
  return truncateGeoJSONDecimals(simplifiedGeojson, config);
}
function getAgencyGeoJSON(config) {
  const shapesGeojsons = getShapesAsGeoJSON();
  const stopsGeojsons = getStopsAsGeoJSON();
  const geojson = mergeGeojson(shapesGeojsons, stopsGeojsons);
  let simplifiedGeojson;
  try {
    simplifiedGeojson = simplify(geojson, {
      tolerance: 1 / 10 ** config.coordinatePrecision,
      highQuality: true
    });
  } catch {
    logWarning(config)("Unable to simplify geojson");
    simplifiedGeojson = geojson;
  }
  return truncateGeoJSONDecimals(simplifiedGeojson, config);
}

// src/lib/template-functions.ts
var template_functions_exports = {};
__export(template_functions_exports, {
  formatTripName: () => formatTripName,
  formatTripNameForCSV: () => formatTripNameForCSV,
  getNotesForStop: () => getNotesForStop,
  getNotesForStoptime: () => getNotesForStoptime,
  getNotesForTimetableLabel: () => getNotesForTimetableLabel,
  getNotesForTrip: () => getNotesForTrip,
  hasNotesOrNotices: () => hasNotesOrNotices,
  timetableHasDifferentDays: () => timetableHasDifferentDays,
  timetablePageHasDifferentDays: () => timetablePageHasDifferentDays,
  timetablePageHasDifferentLabels: () => timetablePageHasDifferentLabels
});
import { every } from "lodash-es";
function timetableHasDifferentDays(timetable) {
  return !every(timetable.orderedTrips, (trip, idx) => {
    if (idx === 0) {
      return true;
    }
    return trip.dayList === timetable.orderedTrips[idx - 1].dayList;
  });
}
function timetablePageHasDifferentDays(timetablePage) {
  return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
    if (idx === 0) {
      return true;
    }
    return timetable.dayListLong === timetablePage.consolidatedTimetables[idx - 1].dayListLong;
  });
}
function timetablePageHasDifferentLabels(timetablePage) {
  return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
    if (idx === 0) {
      return true;
    }
    return timetable.timetable_label === timetablePage.consolidatedTimetables[idx - 1].timetable_label;
  });
}
function hasNotesOrNotices(timetable) {
  return timetable.requestPickupSymbolUsed || timetable.noPickupSymbolUsed || timetable.requestDropoffSymbolUsed || timetable.noDropoffSymbolUsed || timetable.noServiceSymbolUsed || timetable.interpolatedStopSymbolUsed || timetable.notes.length > 0;
}
function getNotesForTimetableLabel(notes) {
  return notes.filter((note) => !note.stop_id && !note.trip_id);
}
function getNotesForStop(notes, stop) {
  return notes.filter((note) => {
    if (note.trip_id) {
      return false;
    }
    if (note.stop_sequence && !stop.trips.some((trip) => trip.stop_sequence === note.stop_sequence)) {
      return false;
    }
    return note.stop_id === stop.stop_id;
  });
}
function getNotesForTrip(notes, trip) {
  return notes.filter((note) => {
    if (note.stop_id) {
      return false;
    }
    return note.trip_id === trip.trip_id;
  });
}
function getNotesForStoptime(notes, stoptime) {
  return notes.filter((note) => {
    if (!note.trip_id && note.stop_id === stoptime.stop_id && note.show_on_stoptime === 1) {
      return true;
    }
    if (!note.stop_id && note.trip_id === stoptime.trip_id && note.show_on_stoptime === 1) {
      return true;
    }
    return note.trip_id === stoptime.trip_id && note.stop_id === stoptime.stop_id;
  });
}
function formatTripName(trip, index, timetable) {
  let tripName;
  if (timetable.routes.length > 1) {
    tripName = trip.route_short_name;
  } else if (timetable.orientation === "horizontal") {
    if (trip.trip_short_name) {
      tripName = trip.trip_short_name;
    } else {
      tripName = `Run #${index + 1}`;
    }
  }
  if (timetableHasDifferentDays(timetable)) {
    tripName += ` ${trip.dayList}`;
  }
  return tripName;
}
function formatTripNameForCSV(trip, timetable) {
  let tripName = "";
  if (timetable.routes.length > 1) {
    tripName += `${trip.route_short_name} - `;
  }
  if (trip.trip_short_name) {
    tripName += trip.trip_short_name;
  } else {
    tripName += trip.trip_id;
  }
  if (trip.trip_headsign) {
    tripName += ` - ${trip.trip_headsign}`;
  }
  if (timetableHasDifferentDays(timetable)) {
    tripName += ` - ${trip.dayList}`;
  }
  return tripName;
}

// package.json
var package_default = {
  name: "gtfs-to-html",
  version: "2.12.3",
  private: false,
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
  keywords: [
    "transit",
    "gtfs",
    "gtfs-realtime",
    "transportation",
    "timetables"
  ],
  homepage: "https://gtfstohtml.com",
  bugs: {
    url: "https://github.com/blinktaginc/gtfs-to-html/issues"
  },
  repository: "git://github.com/blinktaginc/gtfs-to-html",
  license: "MIT",
  author: "Brendan Nee <brendan@blinktag.com>",
  contributors: [
    "Evan Siroky <evan.siroky@yahoo.com>",
    "Nathan Selikoff",
    "Aaron Antrim <aaron@trilliumtransit.com>",
    "Thomas Craig <thomas@trilliumtransit.com>",
    "Holly Kvalheim",
    "Pawajoro",
    "Andrea Mignone",
    "Evo Stamatov",
    "Sebastian Knopf"
  ],
  type: "module",
  main: "./dist/index.js",
  types: "./dist/index.d.ts",
  files: [
    "dist",
    "docker",
    "examples",
    "scripts",
    "views/default",
    "config-sample.json"
  ],
  bin: {
    "gtfs-to-html": "dist/bin/gtfs-to-html.js"
  },
  scripts: {
    build: "tsup",
    postbuild: "node scripts/postinstall.js",
    start: "node ./dist/app",
    prepare: "husky && npm run build",
    postinstall: "node scripts/postinstall.js"
  },
  dependencies: {
    "@maplibre/maplibre-gl-geocoder": "^1.9.1",
    "@turf/helpers": "^7.3.1",
    "@turf/simplify": "^7.3.1",
    anchorme: "^3.0.8",
    archiver: "^7.0.1",
    "cli-table": "^0.3.11",
    "css.escape": "^1.5.1",
    "csv-stringify": "^6.6.0",
    express: "^5.2.1",
    gtfs: "^4.18.2",
    "gtfs-realtime-pbf-js-module": "^1.0.0",
    "js-beautify": "^1.15.4",
    "lodash-es": "^4.17.21",
    "maplibre-gl": "^5.14.0",
    marked: "^17.0.1",
    moment: "^2.30.1",
    pbf: "^4.0.1",
    "pretty-error": "^4.0.0",
    pug: "^3.0.3",
    puppeteer: "^24.32.0",
    "sanitize-filename": "^1.6.3",
    "sanitize-html": "^2.17.0",
    sqlstring: "^2.3.3",
    toposort: "^2.0.2",
    yargs: "^18.0.0",
    yoctocolors: "^2.1.2"
  },
  devDependencies: {
    "@types/archiver": "^7.0.0",
    "@types/cli-table": "^0.3.4",
    "@types/express": "^5.0.6",
    "@types/insane": "^1.0.0",
    "@types/js-beautify": "^1.14.3",
    "@types/lodash-es": "^4.17.12",
    "@types/morgan": "^1.9.10",
    "@types/node": "^24",
    "@types/pug": "^2.0.10",
    "@types/sanitize-html": "^2.16.0",
    "@types/sqlstring": "^2.3.2",
    "@types/toposort": "^2.0.7",
    "@types/yargs": "^17.0.35",
    husky: "^9.1.7",
    "lint-staged": "^16.2.7",
    prettier: "^3.7.4",
    tsup: "^8.5.1",
    typescript: "^5.9.3"
  },
  engines: {
    node: ">= 22"
  },
  "release-it": {
    github: {
      release: true
    },
    plugins: {
      "@release-it/keep-a-changelog": {
        filename: "CHANGELOG.md"
      }
    },
    hooks: {
      "after:bump": "npm run build"
    }
  },
  prettier: {
    singleQuote: true
  },
  "lint-staged": {
    "*.js": "prettier --write",
    "*.ts": "prettier --write",
    "*.json": "prettier --write"
  }
};

// src/lib/utils.ts
var { version } = package_default;
var isTimepoint = (stoptime) => {
  if (isNullOrEmpty(stoptime.timepoint)) {
    return !isNullOrEmpty(stoptime.arrival_time) && !isNullOrEmpty(stoptime.departure_time);
  }
  return stoptime.timepoint === 1;
};
var getLongestTripStoptimes = (trips, config) => {
  const filteredTripStoptimes = trips.map(
    (trip) => trip.stoptimes.filter((stoptime) => {
      if (config.showOnlyTimepoint === true) {
        return isTimepoint(stoptime);
      }
      return true;
    })
  );
  return maxBy(filteredTripStoptimes, (stoptimes) => size(stoptimes));
};
var findCommonStopId = (trips, config) => {
  const longestTripStoptimes = getLongestTripStoptimes(trips, config);
  if (!longestTripStoptimes) {
    return null;
  }
  const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
    if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes)?.stop_id) {
      return false;
    }
    if (isNullOrEmpty(stoptime.arrival_time)) {
      return false;
    }
    return every2(
      trips,
      (trip) => trip.stoptimes.find(
        (tripStoptime) => tripStoptime.stop_id === stoptime.stop_id && tripStoptime.arrival_time !== null
      )
    );
  });
  return commonStoptime ? commonStoptime.stop_id : null;
};
var deduplicateTrips = (trips) => {
  if (trips.length <= 1) {
    return trips;
  }
  const uniqueTrips = /* @__PURE__ */ new Map();
  for (const trip of trips) {
    const tripSignature = trip.stoptimes.map(
      (stoptime) => `${stoptime.stop_id}|${stoptime.departure_time}|${stoptime.arrival_time}`
    ).join("|");
    if (!uniqueTrips.has(tripSignature)) {
      uniqueTrips.set(tripSignature, trip);
    }
  }
  return Array.from(uniqueTrips.values());
};
var sortTrips = (trips, config) => {
  let sortedTrips;
  let commonStopId;
  if (config.sortingAlgorithm === "common") {
    commonStopId = findCommonStopId(trips, config);
    if (commonStopId) {
      sortedTrips = sortTripsByStoptimeAtStop(trips, commonStopId);
    } else {
      sortedTrips = sortTrips(trips, {
        ...config,
        sortingAlgorithm: "beginning"
      });
    }
  } else if (config.sortingAlgorithm === "beginning") {
    for (const trip of trips) {
      if (trip.stoptimes.length === 0) {
        continue;
      }
      trip.firstStoptime = timeToSeconds(trip.stoptimes[0].departure_time);
      trip.lastStoptime = timeToSeconds(
        trip.stoptimes[trip.stoptimes.length - 1].departure_time
      );
    }
    sortedTrips = sortBy(trips, ["firstStoptime", "lastStoptime"]);
  } else if (config.sortingAlgorithm === "end") {
    for (const trip of trips) {
      if (trip.stoptimes.length === 0) {
        continue;
      }
      trip.firstStoptime = timeToSeconds(trip.stoptimes[0].departure_time);
      trip.lastStoptime = timeToSeconds(
        trip.stoptimes[trip.stoptimes.length - 1].departure_time
      );
    }
    sortedTrips = sortBy(trips, ["lastStoptime", "firstStoptime"]);
  } else if (config.sortingAlgorithm === "first") {
    const longestTripStoptimes = getLongestTripStoptimes(trips, config);
    const firstStopId = first(longestTripStoptimes).stop_id;
    sortedTrips = sortTripsByStoptimeAtStop(trips, firstStopId);
  } else if (config.sortingAlgorithm === "last") {
    const longestTripStoptimes = getLongestTripStoptimes(trips, config);
    const lastStopId = last(longestTripStoptimes).stop_id;
    sortedTrips = sortTripsByStoptimeAtStop(trips, lastStopId);
  }
  return sortedTrips ?? [];
};
var sortTripsByStoptimeAtStop = (trips, stopId) => sortBy(trips, (trip) => {
  const stoptime = find(trip.stoptimes, { stop_id: stopId });
  return stoptime ? timeToSeconds(stoptime.departure_time) : void 0;
});
var getCalendarDatesForTimetable = (timetable, config) => {
  const calendarDates = getCalendarDates(
    {
      service_id: timetable.service_ids
    },
    [],
    [["date", "ASC"]]
  );
  const start = moment2(timetable.start_date, "YYYYMMDD");
  const end = moment2(timetable.end_date, "YYYYMMDD");
  const excludedDates = /* @__PURE__ */ new Set();
  const includedDates = /* @__PURE__ */ new Set();
  for (const calendarDate of calendarDates) {
    if (moment2(calendarDate.date, "YYYYMMDD").isBetween(
      start,
      end,
      void 0,
      "[]"
    )) {
      if (calendarDate.exception_type === 1) {
        includedDates.add(formatDate(calendarDate, config.dateFormat));
      } else if (calendarDate.exception_type === 2) {
        excludedDates.add(formatDate(calendarDate, config.dateFormat));
      }
    }
  }
  const includedAndExcludedDates = new Set(
    [...excludedDates].filter((date) => includedDates.has(date))
  );
  return {
    excludedDates: [...excludedDates].filter(
      (date) => !includedAndExcludedDates.has(date)
    ),
    includedDates: [...includedDates].filter(
      (date) => !includedAndExcludedDates.has(date)
    )
  };
};
var getDaysFromCalendars = (calendars) => {
  const days2 = {
    monday: 0,
    tuesday: 0,
    wednesday: 0,
    thursday: 0,
    friday: 0,
    saturday: 0,
    sunday: 0
  };
  for (const calendar of calendars) {
    for (const day of Object.keys(days2)) {
      days2[day] = days2[day] | calendar[day];
    }
  }
  return days2;
};
var getDirectionHeadsignFromTimetable = (timetable) => {
  const trips = getTrips(
    {
      direction_id: timetable.direction_id,
      route_id: timetable.route_ids
    },
    ["trip_headsign"]
  );
  if (trips.length === 0) {
    return "";
  }
  const mostCommonHeadsign = flow(
    countBy,
    entries,
    partialRight(maxBy, last),
    head
  )(compact(trips.map((trip) => trip.trip_headsign)));
  return mostCommonHeadsign;
};
var getTimetableNotesForTimetable = (timetable, config) => {
  const noteReferences = [
    // Get all notes for this timetable.
    ...getTimetableNotesReferences({
      timetable_id: timetable.timetable_id
    }),
    // Get all notes for this route.
    ...getTimetableNotesReferences({
      route_id: timetable.routes.map((route) => route.route_id),
      timetable_id: null
    }),
    // Get all notes for all trips in this timetable.
    ...getTimetableNotesReferences({
      trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
    }),
    // Get all notes for all stops in this timetable.
    ...getTimetableNotesReferences({
      stop_id: timetable.stops.map((stop) => stop.stop_id),
      trip_id: null,
      route_id: null,
      timetable_id: null
    })
  ];
  const usedNoteReferences = [];
  for (const noteReference of noteReferences) {
    if (noteReference.stop_sequence === "" || noteReference.stop_sequence === null) {
      usedNoteReferences.push(noteReference);
      continue;
    }
    if (noteReference.stop_id === "" || noteReference.stop_id === null) {
      timetable.warnings.push(
        `Timetable Note Reference for note_id=${noteReference.note_id} has a \`stop_sequence\` but no \`stop_id\` - ignoring`
      );
      continue;
    }
    const stop = timetable.stops.find(
      (stop2) => stop2.stop_id === noteReference.stop_id
    );
    if (!stop) {
      continue;
    }
    const tripWithMatchingStopSequence = stop.trips.find(
      (trip) => trip.stop_sequence === noteReference.stop_sequence
    );
    if (tripWithMatchingStopSequence) {
      usedNoteReferences.push(noteReference);
    }
  }
  const notes = getTimetableNotes({
    note_id: usedNoteReferences.map((noteReference) => noteReference.note_id)
  });
  const symbols = "abcdefghijklmnopqrstuvwxyz".split("");
  let symbolIndex = 0;
  for (const note of notes) {
    if (note.symbol === "" || note.symbol === null) {
      note.symbol = symbolIndex < symbols.length - 1 ? symbols[symbolIndex] : symbolIndex - symbols.length;
      symbolIndex += 1;
    }
  }
  const formattedNotes = usedNoteReferences.map((noteReference) => ({
    ...noteReference,
    ...notes.find((note) => note.note_id === noteReference.note_id)
  }));
  return sortBy(formattedNotes, "symbol");
};
var createTimetablePage = ({
  timetablePageId,
  timetables,
  config
}) => {
  const updatedTimetables = timetables.map((timetable) => {
    if (!timetable.routes) {
      timetable.routes = getRoutes({
        route_id: timetable.route_ids
      });
    }
    return timetable;
  });
  const timetablePage = {
    timetable_page_id: timetablePageId,
    timetables: updatedTimetables,
    routes: updatedTimetables.flatMap((timetable) => timetable.routes)
  };
  const filename = generateTimetablePageFileName(timetablePage, config);
  return {
    ...timetablePage,
    filename
  };
};
var createTimetable = ({
  route,
  directionId,
  tripHeadsign,
  calendars,
  calendarDates
}) => {
  const serviceIds = uniq([
    ...calendars?.map((calendar) => calendar.service_id) ?? [],
    ...calendarDates?.map((calendarDate) => calendarDate.service_id) ?? []
  ]);
  const days2 = {
    monday: null,
    tuesday: null,
    wednesday: null,
    thursday: null,
    friday: null,
    saturday: null,
    sunday: null
  };
  let startDate = null;
  let endDate = null;
  if (calendars && calendars.length > 0) {
    Object.assign(days2, getDaysFromCalendars(calendars));
    startDate = parseInt(
      moment2.min(
        calendars.map((calendar) => moment2(calendar.start_date, "YYYYMMDD"))
      ).format("YYYYMMDD"),
      10
    );
    endDate = parseInt(
      moment2.max(calendars.map((calendar) => moment2(calendar.end_date, "YYYYMMDD"))).format("YYYYMMDD"),
      10
    );
  }
  const timetableId = formatTimetableId({
    routeIds: [route.route_id],
    directionId,
    days: days2,
    dates: calendarDates?.map((calendarDate) => calendarDate.date)
  });
  return {
    timetable_id: timetableId,
    route_ids: [route.route_id],
    direction_id: directionId === null ? null : directionId,
    direction_name: tripHeadsign === null ? null : tripHeadsign,
    routes: [route],
    include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
    service_ids: serviceIds,
    service_notes: null,
    timetable_label: null,
    start_time: null,
    end_time: null,
    orientation: null,
    timetable_sequence: null,
    show_trip_continuation: null,
    start_date: startDate,
    end_date: endDate,
    ...days2
  };
};
var convertRoutesToTimetablePages = (config) => {
  const routes = getRoutes();
  const timetablePages = [];
  const { calendars, calendarDates } = getCalendarsFromConfig(config);
  for (const route of routes) {
    const trips = getTrips(
      {
        route_id: route.route_id
      },
      ["trip_headsign", "direction_id", "trip_id", "service_id"]
    );
    const uniqueTripDirections = orderBy(
      uniqBy(trips, (trip) => trip.direction_id),
      "direction_id"
    );
    const sortedCalendars = orderBy(calendars, calendarToCalendarCode, "desc");
    const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
    const calendarDateGroups = groupBy(calendarDates, "service_id");
    const timetables = [];
    for (const uniqueTripDirection of uniqueTripDirections) {
      for (const calendars2 of Object.values(calendarGroups)) {
        const tripsForCalendars = trips.filter(
          (trip) => some(calendars2, { service_id: trip.service_id })
        );
        if (tripsForCalendars.length > 0) {
          timetables.push(
            createTimetable({
              route,
              directionId: uniqueTripDirection.direction_id,
              tripHeadsign: uniqueTripDirection.trip_headsign,
              calendars: calendars2
            })
          );
        }
      }
      for (const calendarDates2 of Object.values(calendarDateGroups)) {
        const tripsForCalendarDates = trips.filter(
          (trip) => some(calendarDates2, { service_id: trip.service_id })
        );
        if (tripsForCalendarDates.length > 0) {
          timetables.push(
            createTimetable({
              route,
              directionId: uniqueTripDirection.direction_id,
              tripHeadsign: uniqueTripDirection.trip_headsign,
              calendarDates: calendarDates2
            })
          );
        }
      }
    }
    if (timetables.length === 0) {
      continue;
    }
    if (config.groupTimetablesIntoPages === true) {
      timetablePages.push(
        createTimetablePage({
          timetablePageId: `route_${route.route_short_name ?? route.route_long_name}`,
          timetables,
          config
        })
      );
    } else {
      for (const timetable of timetables) {
        timetablePages.push(
          createTimetablePage({
            timetablePageId: timetable.timetable_id,
            timetables: [timetable],
            config
          })
        );
      }
    }
  }
  return timetablePages;
};
var generateTripsByFrequencies = (trip, frequencies, config) => {
  const formattedFrequencies = frequencies.map(
    (frequency) => formatFrequency(frequency, config)
  );
  const resetTrip = resetStoptimesToMidnight(trip);
  const trips = [];
  for (const frequency of formattedFrequencies) {
    const startSeconds = secondsAfterMidnight(frequency.start_time);
    const endSeconds = secondsAfterMidnight(frequency.end_time);
    for (let offset = startSeconds; offset < endSeconds; offset += frequency.headway_secs) {
      const newTrip = cloneDeep(resetTrip);
      trips.push({
        ...newTrip,
        trip_id: `${resetTrip.trip_id}_freq_${trips.length}`,
        stoptimes: updateStoptimesByOffset(newTrip, offset)
      });
    }
  }
  return trips;
};
var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config) => {
  if (config.showArrivalOnDifference === null || config.showArrivalOnDifference === void 0) {
    return stopIds;
  }
  for (const trip of timetable.orderedTrips) {
    for (const stoptime of trip.stoptimes) {
      const timepointDifference = fromGTFSTime(stoptime.departure_time).diff(
        fromGTFSTime(stoptime.arrival_time),
        "minutes"
      );
      if (timepointDifference < config.showArrivalOnDifference) {
        continue;
      }
      const index = stopIds.indexOf(stoptime.stop_id);
      if (index === 0 || index === stopIds.length - 1) {
        continue;
      }
      if (stoptime.stop_id === stopIds[index + 1] || stoptime.stop_id === stopIds[index - 1]) {
        continue;
      }
      stopIds.splice(index, 0, stoptime.stop_id);
    }
  }
  return stopIds;
};
var getStopOrder = (timetable, config) => {
  const timetableStopOrders = getTimetableStopOrders(
    {
      timetable_id: timetable.timetable_id
    },
    ["stop_id"],
    [["stop_sequence", "ASC"]]
  );
  if (timetableStopOrders.length > 0) {
    return timetableStopOrders.map(
      (timetableStopOrder) => timetableStopOrder.stop_id
    );
  }
  try {
    const stopGraph = [];
    const timepointStopIds = new Set(
      timetable.orderedTrips.flatMap(
        (trip) => trip.stoptimes.filter((stoptime) => isTimepoint(stoptime)).map((stoptime) => stoptime.stop_id)
      )
    );
    for (const trip of timetable.orderedTrips) {
      const sortedStopIds = trip.stoptimes.filter((stoptime) => {
        if (config.showOnlyTimepoint === true) {
          return timepointStopIds.has(stoptime.stop_id);
        }
        return true;
      }).map((stoptime) => stoptime.stop_id);
      for (const [index, stopId] of sortedStopIds.entries()) {
        if (index === sortedStopIds.length - 1) {
          continue;
        }
        stopGraph.push([stopId, sortedStopIds[index + 1]]);
      }
    }
    if (stopGraph.length === 0 && config.showOnlyTimepoint === true) {
      timetable.warnings.push(
        `Timetable ${timetable.timetable_id}'s trips have stoptimes with timepoints but \`showOnlyTimepoint\` is true. Try setting \`showOnlyTimepoint\` to false.`
      );
    }
    const stopIds = toposort(stopGraph);
    return duplicateStopsForDifferentArrivalDeparture(
      stopIds,
      timetable,
      config
    );
  } catch {
    const longestTripStoptimes = getLongestTripStoptimes(
      timetable.orderedTrips,
      config
    );
    const stopIds = longestTripStoptimes.map(
      (stoptime) => stoptime.stop_id
    );
    const missingStopIds = difference(
      new Set(
        timetable.orderedTrips.flatMap(
          (trip) => trip.stoptimes.map((stoptime) => stoptime.stop_id)
        )
      ),
      new Set(stopIds)
    );
    if (missingStopIds.length > 0) {
      timetable.warnings.push(
        `Timetable ${timetable.timetable_id} stops are unable to be topologically sorted and has no \`timetable_stop_order.txt\`. Falling back to using the using the stop order from trip with most stoptimes, but this does not include stop_ids ${formatListForDisplay(missingStopIds)}. Try manually specifying stops with \`timetable_stop_order.txt\`. Read more at https://gtfstohtml.com/docs/timetable-stop-order`
      );
    }
    return duplicateStopsForDifferentArrivalDeparture(
      stopIds,
      timetable,
      config
    );
  }
};
var getStopsForTimetable = (timetable, config) => {
  if (timetable.orderedTrips.length === 0) {
    return [];
  }
  const orderedStopIds = getStopOrder(timetable, config);
  const orderedStops = orderedStopIds.map((stopId, index) => {
    const stops = getStops({
      stop_id: stopId
    });
    if (stops.length === 0) {
      throw new Error(
        `No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`
      );
    }
    const stop = {
      ...stops[0],
      trips: []
    };
    if (index < orderedStopIds.length - 1 && stopId === orderedStopIds[index + 1]) {
      stop.type = "arrival";
    } else if (index > 0 && stopId === orderedStopIds[index - 1]) {
      stop.type = "departure";
    }
    return stop;
  });
  if (config.showStopCity) {
    const stopAttributes = getStopAttributes({
      stop_id: orderedStopIds
    });
    for (const stopAttribute of stopAttributes) {
      const stop = orderedStops.find(
        (stop2) => stop2.stop_id === stopAttribute.stop_id
      );
      if (stop) {
        stop.stop_city = stopAttribute.stop_city;
      }
    }
  }
  return orderedStops;
};
var getCalendarsFromConfig = (config) => {
  const db = openDb();
  let whereClause = "";
  const whereClauses = [];
  if (config.endDate) {
    if (!moment2(config.endDate).isValid()) {
      throw new Error(`Invalid endDate=${config.endDate} in config.json`);
    }
    whereClauses.push(
      `start_date <= ${sqlString.escape(moment2(config.endDate).format("YYYYMMDD"))}`
    );
  }
  if (config.startDate) {
    if (!moment2(config.startDate).isValid()) {
      throw new Error(`Invalid startDate=${config.startDate} in config.json`);
    }
    whereClauses.push(
      `end_date >= ${sqlString.escape(moment2(config.startDate).format("YYYYMMDD"))}`
    );
  }
  if (whereClauses.length > 0) {
    whereClause = `WHERE ${whereClauses.join(" AND ")}`;
  }
  const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
  const serviceIds = calendars.map((calendar) => calendar.service_id);
  const calendarDates = db.prepare(
    `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
  ).all();
  return {
    calendars,
    calendarDates
  };
};
var getCalendarsFromTimetable = (timetable) => {
  const db = openDb();
  let whereClause = "";
  const whereClauses = [];
  if (timetable.end_date) {
    if (!moment2(timetable.end_date, "YYYYMMDD", true).isValid()) {
      throw new Error(
        `Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`
      );
    }
    whereClauses.push(`start_date <= ${sqlString.escape(timetable.end_date)}`);
  }
  if (timetable.start_date) {
    if (!moment2(timetable.start_date, "YYYYMMDD", true).isValid()) {
      throw new Error(
        `Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`
      );
    }
    whereClauses.push(`end_date >= ${sqlString.escape(timetable.start_date)}`);
  }
  const days2 = getDaysFromCalendars([timetable]);
  const dayQueries = reduce(
    days2,
    (memo, value, key) => {
      if (value === 1) {
        memo.push(`${key} = 1`);
      }
      return memo;
    },
    []
  );
  if (dayQueries.length > 0) {
    whereClauses.push(`(${dayQueries.join(" OR ")})`);
  }
  if (whereClauses.length > 0) {
    whereClause = `WHERE ${whereClauses.join(" AND ")}`;
  }
  return db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
};
var getCalendarDatesForDateRange = (startDate, endDate) => {
  const db = openDb();
  const whereClauses = [];
  if (endDate) {
    whereClauses.push(`date <= ${sqlString.escape(endDate)}`);
  }
  if (startDate) {
    whereClauses.push(`date >= ${sqlString.escape(startDate)}`);
  }
  const calendarDates = db.prepare(
    `SELECT service_id, date, exception_type FROM calendar_dates WHERE ${whereClauses.join(
      " AND "
    )}`
  ).all();
  return calendarDates;
};
var getAllStationStopIds = (stopId) => {
  const stops = getStops({
    stop_id: stopId
  });
  if (stops.length === 0) {
    throw new Error(`No stop found for stop_id=${stopId}`);
  }
  const stop = stops[0];
  if (isNullOrEmpty(stop.parent_station)) {
    return [stopId];
  }
  const stopsInParentStation = getStops(
    {
      parent_station: stop.parent_station
    },
    ["stop_id"]
  );
  return [
    stop.parent_station,
    ...stopsInParentStation.map((stop2) => stop2.stop_id)
  ];
};
var getTripsWithSameBlock = (trip, timetable) => {
  const trips = getTrips(
    {
      block_id: trip.block_id,
      service_id: timetable.service_ids
    },
    ["trip_id", "route_id"]
  );
  for (const blockTrip of trips) {
    const stopTimes = getStoptimes(
      {
        trip_id: blockTrip.trip_id
      },
      [],
      [["stop_sequence", "ASC"]]
    );
    if (stopTimes.length === 0) {
      throw new Error(
        `No stoptimes found found for trip_id=${blockTrip.trip_id}`
      );
    }
    blockTrip.firstStoptime = first(stopTimes);
    blockTrip.lastStoptime = last(stopTimes);
  }
  return sortBy(trips, (trip2) => trip2.firstStoptime.departure_timestamp);
};
var addTripContinuation = (trip, timetable) => {
  if (!trip.block_id || trip.stoptimes.length === 0) {
    return;
  }
  const maxContinuesAsWaitingTimeSeconds = 60 * 60;
  const firstStoptime = first(trip.stoptimes);
  const firstStopIds = getAllStationStopIds(firstStoptime.stop_id);
  const lastStoptime = last(trip.stoptimes);
  const lastStopIds = getAllStationStopIds(lastStoptime.stop_id);
  const blockTrips = getTripsWithSameBlock(trip, timetable);
  const previousTrip = findLast(
    blockTrips,
    (blockTrip) => blockTrip.lastStoptime.arrival_timestamp <= firstStoptime.departure_timestamp
  );
  if (previousTrip && previousTrip.route_id !== trip.route_id && previousTrip.lastStoptime.arrival_timestamp >= firstStoptime.departure_timestamp - maxContinuesAsWaitingTimeSeconds && firstStopIds.includes(previousTrip.lastStoptime.stop_id)) {
    const routes = getRoutes({
      route_id: previousTrip.route_id
    });
    previousTrip.route = routes[0];
    trip.continues_from_route = previousTrip;
  }
  const nextTrip = find(
    blockTrips,
    (blockTrip) => blockTrip.firstStoptime.departure_timestamp >= lastStoptime.arrival_timestamp
  );
  if (nextTrip && nextTrip.route_id !== trip.route_id && nextTrip.firstStoptime.departure_timestamp <= lastStoptime.arrival_timestamp + maxContinuesAsWaitingTimeSeconds && lastStopIds.includes(nextTrip.firstStoptime.stop_id)) {
    const routes = getRoutes({
      route_id: nextTrip.route_id
    });
    nextTrip.route = routes[0];
    trip.continues_as_route = nextTrip;
  }
};
var filterTrips = (timetable, config) => {
  let filteredTrips = timetable.orderedTrips;
  for (const trip of filteredTrips) {
    const combinedStoptimes = [];
    for (const [index, stoptime] of trip.stoptimes.entries()) {
      if (index === 0 || stoptime.stop_id !== trip.stoptimes[index - 1].stop_id) {
        combinedStoptimes.push(stoptime);
      } else {
        combinedStoptimes[combinedStoptimes.length - 1].departure_time = stoptime.departure_time;
      }
    }
    trip.stoptimes = combinedStoptimes;
  }
  const timetableStopIds = new Set(
    timetable.stops.map((stop) => stop.stop_id)
  );
  for (const trip of filteredTrips) {
    trip.stoptimes = trip.stoptimes.filter(
      (stoptime) => timetableStopIds.has(stoptime.stop_id)
    );
  }
  filteredTrips = filteredTrips.filter(
    (trip) => trip.stoptimes.length > 1
  );
  if (config.showDuplicateTrips === false) {
    filteredTrips = deduplicateTrips(filteredTrips);
  }
  return filteredTrips;
};
var getTripsForTimetable = (timetable, calendars, config) => {
  const tripQuery = {
    route_id: timetable.route_ids,
    service_id: timetable.service_ids
  };
  if (!isNullOrEmpty(timetable.direction_id)) {
    tripQuery.direction_id = timetable.direction_id;
  }
  const trips = getTrips(tripQuery);
  if (trips.length === 0) {
    timetable.warnings.push(
      `No trips found for route_id=${timetable.route_ids.join(
        "_"
      )}, direction_id=${timetable.direction_id}, service_ids=${JSON.stringify(
        timetable.service_ids
      )}, timetable_id=${timetable.timetable_id}`
    );
  }
  const frequencies = getFrequencies({
    trip_id: trips.map((trip) => trip.trip_id)
  });
  timetable.service_ids = uniq(trips.map((trip) => trip.service_id));
  const formattedTrips = [];
  for (const trip of trips) {
    const formattedTrip = formatTrip(trip, timetable, calendars, config);
    formattedTrip.stoptimes = getStoptimes(
      {
        trip_id: formattedTrip.trip_id
      },
      [],
      [["stop_sequence", "ASC"]]
    );
    if (formattedTrip.stoptimes.length === 0) {
      timetable.warnings.push(
        `No stoptimes found for trip_id=${formattedTrip.trip_id}, route_id=${timetable.route_ids.join("_")}, timetable_id=${timetable.timetable_id}`
      );
    }
    if (timetable.start_timestamp !== "" && timetable.start_timestamp !== null && timetable.start_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp < timetable.start_timestamp) {
      return;
    }
    if (timetable.end_timestamp !== "" && timetable.end_timestamp !== null && timetable.end_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp >= timetable.end_timestamp) {
      return;
    }
    if (timetable.show_trip_continuation) {
      addTripContinuation(formattedTrip, timetable);
      if (formattedTrip.continues_as_route) {
        timetable.has_continues_as_route = true;
      }
      if (formattedTrip.continues_from_route) {
        timetable.has_continues_from_route = true;
      }
    }
    const tripFrequencies = frequencies.filter(
      (frequency) => frequency.trip_id === trip.trip_id
    );
    if (tripFrequencies.length === 0) {
      formattedTrips.push(formattedTrip);
    } else {
      const frequencyTrips = generateTripsByFrequencies(
        formattedTrip,
        frequencies,
        config
      );
      formattedTrips.push(...frequencyTrips);
      timetable.frequencies = frequencies;
      timetable.frequencyExactTimes = some(frequencies, {
        exact_times: 1
      });
    }
  }
  if (config.useParentStation) {
    const stopIds = [];
    for (const trip of formattedTrips) {
      for (const stoptime of trip.stoptimes) {
        stopIds.push(stoptime.stop_id);
      }
    }
    const stops = getStops(
      {
        stop_id: uniq(stopIds)
      },
      ["parent_station", "stop_id"]
    );
    for (const trip of formattedTrips) {
      for (const stoptime of trip.stoptimes) {
        const stop = stops.find((stop2) => stop2.stop_id === stoptime.stop_id);
        if (stop?.parent_station) {
          stoptime.stop_id = stop.parent_station;
        }
      }
    }
  }
  return sortTrips(formattedTrips, config);
};
var formatTimetables = (timetables, config) => {
  const formattedTimetables = timetables.map((timetable) => {
    timetable.warnings = [];
    const dayList = formatDays(timetable, config);
    const calendars = getCalendarsFromTimetable(timetable);
    const serviceIds = /* @__PURE__ */ new Set();
    for (const calendar of calendars) {
      serviceIds.add(calendar.service_id);
    }
    if (timetable.include_exceptions === 1) {
      const calendarDates = getCalendarDatesForDateRange(
        timetable.start_date,
        timetable.end_date
      );
      const calendarDateGroups = groupBy(calendarDates, "service_id");
      for (const [serviceId, calendarDateGroup] of Object.entries(
        calendarDateGroups
      )) {
        const calendar = calendars.find(
          (c) => c.service_id === serviceId
        );
        if (calendarDateGroup.some(
          (calendarDate) => calendarDate.exception_type === 1
        )) {
          serviceIds.add(serviceId);
        }
        const calendarDateGroupExceptionType2 = calendarDateGroup.filter(
          (calendarDate) => calendarDate.exception_type === 2
        );
        if (timetable.start_date && timetable.end_date && calendar && calendarDateGroupExceptionType2.length > 0) {
          const datesDuringDateRange = calendarToDateList(
            calendar,
            timetable.start_date,
            timetable.end_date
          );
          if (datesDuringDateRange.length === 0) {
            serviceIds.delete(serviceId);
          }
          const everyDateIsExcluded = datesDuringDateRange.every(
            (dateDuringDateRange) => calendarDateGroupExceptionType2.some(
              (calendarDate) => calendarDate.date === dateDuringDateRange
            )
          );
          if (everyDateIsExcluded) {
            serviceIds.delete(serviceId);
          }
        }
      }
    }
    Object.assign(timetable, {
      noServiceSymbolUsed: false,
      requestDropoffSymbolUsed: false,
      noDropoffSymbolUsed: false,
      requestPickupSymbolUsed: false,
      noPickupSymbolUsed: false,
      interpolatedStopSymbolUsed: false,
      showStopCity: config.showStopCity,
      showStopDescription: config.showStopDescription,
      noServiceSymbol: config.noServiceSymbol,
      requestDropoffSymbol: config.requestDropoffSymbol,
      noDropoffSymbol: config.noDropoffSymbol,
      requestPickupSymbol: config.requestPickupSymbol,
      noPickupSymbol: config.noPickupSymbol,
      interpolatedStopSymbol: config.interpolatedStopSymbol,
      orientation: timetable.orientation || config.defaultOrientation,
      service_ids: Array.from(serviceIds),
      dayList,
      dayListLong: formatDaysLong(dayList, config)
    });
    timetable.orderedTrips = getTripsForTimetable(timetable, calendars, config);
    timetable.stops = getStopsForTimetable(timetable, config);
    timetable.calendarDates = getCalendarDatesForTimetable(timetable, config);
    timetable.timetable_label = formatTimetableLabel(timetable);
    timetable.notes = getTimetableNotesForTimetable(timetable, config);
    if (config.showMap) {
      timetable.geojson = getTimetableGeoJSON(timetable, config);
    }
    timetable.trip_ids = uniq(
      timetable.orderedTrips.map((trip) => trip.trip_id)
    );
    timetable.orderedTrips = filterTrips(timetable, config);
    timetable.stops = formatStops(timetable, config);
    return timetable;
  });
  if (config.allowEmptyTimetables) {
    return formattedTimetables;
  }
  return formattedTimetables.filter(
    (timetable) => timetable.orderedTrips.length > 0
  );
};
function getTimetablePagesForAgency(config) {
  const timetables = mergeTimetablesWithSameId(getTimetables());
  const routes = getRoutes();
  const formattedTimetables = timetables.map((timetable) => {
    return {
      ...timetable,
      routes: routes.filter(
        (route) => timetable.route_ids.includes(route.route_id)
      )
    };
  });
  if (timetables.length === 0) {
    return convertRoutesToTimetablePages(config);
  }
  const timetablePages = getTimetablePages(
    {},
    [],
    [["timetable_page_id", "ASC"]]
  );
  if (timetablePages.length === 0) {
    return formattedTimetables.map(
      (timetable) => createTimetablePage({
        timetablePageId: timetable.timetable_id,
        timetables: [timetable],
        config
      })
    );
  }
  return timetablePages.map((timetablePage) => {
    return {
      ...timetablePage,
      timetables: sortBy(
        formattedTimetables.filter(
          (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
        ),
        "timetable_sequence"
      )
    };
  });
}
var getDataForTimetablePageById = (timetablePageId) => {
  let calendarCode;
  let calendars;
  let calendarDates;
  let serviceId;
  let directionId = "";
  const parts = timetablePageId?.split("|") ?? [];
  if (parts.length > 2) {
    directionId = Number.parseInt(parts.pop(), 10);
    calendarCode = parts.pop();
  } else if (parts.length > 1) {
    directionId = null;
    calendarCode = parts.pop();
  }
  const routeId = parts.join("|");
  const routes = getRoutes({
    route_id: routeId
  });
  const trips = getTrips(
    {
      route_id: routeId,
      direction_id: directionId
    },
    ["trip_headsign", "direction_id"]
  );
  const uniqueTripDirections = uniqBy(trips, (trip) => trip.direction_id);
  if (uniqueTripDirections.length === 0) {
    throw new Error(
      `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
    );
  }
  if (/^[01]*$/.test(calendarCode ?? "")) {
    calendars = getCalendars({
      ...calendarCodeToCalendar(calendarCode)
    });
  } else {
    serviceId = calendarCode;
    calendarDates = getCalendarDates({
      exception_type: 1,
      service_id: serviceId
    });
  }
  return {
    calendars,
    calendarDates,
    route: routes[0],
    directionId: uniqueTripDirections[0].direction_id,
    tripHeadsign: uniqueTripDirections[0].trip_headsign
  };
};
var getTimetablePageById = (timetablePageId, config) => {
  const timetablePages = getTimetablePages({
    timetable_page_id: timetablePageId
  });
  const timetables = mergeTimetablesWithSameId(
    getTimetables()
  );
  if (timetablePages.length > 1) {
    throw new Error(
      `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
    );
  }
  if (timetablePages.length === 1) {
    const timetablePage = timetablePages[0];
    timetablePage.timetables = sortBy(
      timetables.filter(
        (timetable2) => timetable2.timetable_page_id === timetablePageId
      ),
      "timetable_sequence"
    );
    for (const timetable2 of timetablePage.timetables) {
      timetable2.routes = getRoutes({
        route_id: timetable2.route_ids
      });
    }
    return timetablePage;
  }
  if (timetables.length > 0) {
    const timetablePageTimetables = timetables.filter(
      (timetable2) => timetable2.timetable_id === timetablePageId
    );
    if (timetablePageTimetables.length === 0) {
      throw new Error(
        `No timetable found for timetable_page_id=${timetablePageId}`
      );
    }
    return createTimetablePage({
      timetablePageId,
      timetables: [timetablePageTimetables[0]],
      config
    });
  }
  if (timetablePageId.startsWith("route_")) {
    const routes = getRoutes({
      route_short_name: timetablePageId.split("_")[1]
    });
    if (routes.length === 0) {
      throw new Error(
        `No route found for timetable_page_id=${timetablePageId}`
      );
    }
    const { calendars: calendars2, calendarDates: calendarDates2 } = getCalendarsFromConfig(config);
    const trips = getTrips(
      {
        route_id: routes[0].route_id
      },
      ["trip_headsign", "direction_id", "trip_id", "service_id"]
    );
    const uniqueTripDirections = orderBy(
      uniqBy(trips, (trip) => trip.direction_id),
      "direction_id"
    );
    const sortedCalendars = orderBy(calendars2, calendarToCalendarCode, "desc");
    const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
    const calendarDateGroups = groupBy(calendarDates2, "service_id");
    const timetables2 = [];
    for (const uniqueTripDirection of uniqueTripDirections) {
      for (const calendars3 of Object.values(calendarGroups)) {
        const tripsForCalendars = trips.filter(
          (trip) => some(calendars3, { service_id: trip.service_id })
        );
        if (tripsForCalendars.length > 0) {
          timetables2.push(
            createTimetable({
              route: routes[0],
              directionId: uniqueTripDirection.direction_id,
              tripHeadsign: uniqueTripDirection.trip_headsign,
              calendars: calendars3
            })
          );
        }
      }
      for (const calendarDates3 of Object.values(calendarDateGroups)) {
        const tripsForCalendarDates = trips.filter(
          (trip) => some(calendarDates3, { service_id: trip.service_id })
        );
        if (tripsForCalendarDates.length > 0) {
          timetables2.push(
            createTimetable({
              route: routes[0],
              directionId: uniqueTripDirection.direction_id,
              tripHeadsign: uniqueTripDirection.trip_headsign,
              calendarDates: calendarDates3
            })
          );
        }
      }
    }
    return createTimetablePage({
      timetablePageId,
      timetables: timetables2,
      config
    });
  }
  const { calendars, calendarDates, route, directionId, tripHeadsign } = getDataForTimetablePageById(timetablePageId);
  const timetable = createTimetable({
    route,
    directionId,
    tripHeadsign,
    calendars,
    calendarDates
  });
  return createTimetablePage({
    timetablePageId,
    timetables: [timetable],
    config
  });
};
function setDefaultConfig(initialConfig) {
  const defaults = {
    allowEmptyTimetables: false,
    beautify: false,
    coordinatePrecision: 5,
    dateFormat: "MMM D, YYYY",
    daysShortStrings: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    daysStrings: [
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    ],
    defaultOrientation: "vertical",
    interpolatedStopSymbol: "\u2022",
    interpolatedStopText: "Estimated time of arrival",
    groupTimetablesIntoPages: true,
    gtfsToHtmlVersion: version,
    linkStopUrls: false,
    mapStyleUrl: "https://tiles.openfreemap.org/styles/positron",
    menuType: "jump",
    noDropoffSymbol: "\u2021",
    noDropoffText: "No drop off available",
    noHead: false,
    noPickupSymbol: "**",
    noPickupText: "No pickup available",
    noRegularServiceDaysText: "No regular service days",
    noServiceSymbol: "-",
    noServiceText: "No service at this stop",
    outputFormat: "html",
    overwriteExistingFiles: true,
    requestDropoffSymbol: "\u2020",
    requestDropoffText: "Must request drop off",
    requestPickupSymbol: "***",
    requestPickupText: "Request stop - call for pickup",
    serviceNotProvidedOnText: "Service not provided on",
    serviceProvidedOnText: "Service provided on",
    showArrivalOnDifference: 0.2,
    showCalendarExceptions: true,
    showDuplicateTrips: false,
    showMap: false,
    showOnlyTimepoint: false,
    showRouteTitle: true,
    showStopCity: false,
    showStopDescription: false,
    showStoptimesForRequestStops: true,
    skipImport: false,
    sortingAlgorithm: "common",
    timeFormat: "h:mma",
    useParentStation: true,
    verbose: true,
    zipOutput: false
  };
  const config = Object.assign(defaults, initialConfig);
  if (config.outputFormat === "pdf") {
    config.noHead = false;
    config.menuType = "none";
  }
  config.hasGtfsRealtimeVehiclePositions = config.agencies.some(
    (agency) => agency.realtimeVehiclePositions?.url
  );
  config.hasGtfsRealtimeTripUpdates = config.agencies.some(
    (agency) => agency.realtimeTripUpdates?.url
  );
  config.hasGtfsRealtimeAlerts = config.agencies.some(
    (agency) => agency.realtimeAlerts?.url
  );
  return config;
}
function getFormattedTimetablePage(timetablePageId, config) {
  const timetablePage = getTimetablePageById(
    timetablePageId,
    config
  );
  const consolidatedTimetables = formatTimetables(
    timetablePage.timetables,
    config
  );
  for (const timetable of consolidatedTimetables) {
    if (isNullOrEmpty(timetable.direction_name)) {
      timetable.direction_name = getDirectionHeadsignFromTimetable(timetable);
    }
    if (!timetable.routes) {
      timetable.routes = getRoutes({
        route_id: timetable.route_ids
      });
    }
  }
  const uniqueRoutes = uniqBy(
    flatMap(consolidatedTimetables, (timetable) => timetable.routes),
    "route_id"
  );
  const formattedTimetablePage = {
    ...timetablePage,
    consolidatedTimetables,
    dayList: formatDays(getDaysFromCalendars(consolidatedTimetables), config),
    dayLists: uniq(
      consolidatedTimetables.map((timetable) => timetable.dayList)
    ),
    route_ids: uniqueRoutes.map((route) => route.route_id),
    agency_ids: uniq(compact(uniqueRoutes.map((route) => route.agency_id))),
    filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`,
    timetable_page_label: timetablePage.timetable_page_label ?? formatListForDisplay(uniqueRoutes.map((route) => formatRouteName(route)))
  };
  return formattedTimetablePage;
}
var generateStats = (timetablePage) => {
  const routeIds = {};
  const serviceIds = {};
  const stats = {
    stops: 0,
    trips: 0,
    routes: 0,
    calendars: 0
  };
  for (const timetable of timetablePage.consolidatedTimetables) {
    stats.stops += timetable.stops.length;
    stats.trips += timetable.orderedTrips.length;
    for (const serviceId of timetable.service_ids) {
      serviceIds[serviceId] = true;
    }
    for (const routeId of timetable.route_ids) {
      routeIds[routeId] = true;
    }
  }
  stats.routes = size(routeIds);
  stats.calendars = size(serviceIds);
  return stats;
};
function generateTimetableHTML(timetablePage, config) {
  const agencies = getAgencies2();
  const templateVars = {
    timetablePage,
    config,
    title: `${timetablePage.timetable_page_label} | ${formatListForDisplay(agencies.map((agency) => agency.agency_name))}`
  };
  return renderTemplate("timetablepage", templateVars, config);
}
function generateTimetableCSV(timetable) {
  const lines = [];
  lines.push([
    "",
    ...timetable.orderedTrips.map(
      (trip) => formatTripNameForCSV(trip, timetable)
    )
  ]);
  if (timetable.has_continues_from_route) {
    lines.push([
      "Continues from route",
      ...timetable.orderedTrips.map((trip) => formatTripContinuesFrom(trip))
    ]);
  }
  for (const stop of timetable.stops) {
    lines.push([
      formatStopName(stop),
      ...stop.trips.map((stoptime) => stoptime.formatted_time)
    ]);
  }
  if (timetable.has_continues_as_route) {
    lines.push([
      "Continues as route",
      ...timetable.orderedTrips.map((trip) => formatTripContinuesAs(trip))
    ]);
  }
  if (timetable.orientation === "vertical") {
    return stringify(zip(...lines));
  }
  return stringify(lines);
}
function generateOverviewHTML(timetablePages, config) {
  const agencies = getAgencies2();
  if (agencies.length === 0) {
    throw new Error("No agencies found");
  }
  const geojson = config.showMap ? getAgencyGeoJSON(config) : void 0;
  const templateVars = {
    agency: {
      ...first(agencies),
      geojson
    },
    // Legacy agency object
    agencies,
    geojson,
    config,
    timetablePages,
    title: `${formatListForDisplay(agencies.map((agency) => agency.agency_name))} Timetables`
  };
  return renderTemplate("overview", templateVars, config);
}

// src/lib/formatters.ts
function replaceAll(string, mapObject) {
  const re = new RegExp(Object.keys(mapObject).join("|"), "gi");
  return string.replace(re, (matched) => mapObject[matched]);
}
function isNullOrEmpty(value) {
  return value === null || value === "";
}
function formatDate(date, dateFormat) {
  if (date.holiday_name) {
    return date.holiday_name;
  }
  return moment3(date.date, "YYYYMMDD").format(dateFormat);
}
function timeToSeconds(time) {
  return moment3.duration(time).asSeconds();
}
function formatStopTime(stoptime, timetable, config) {
  stoptime.classes = [];
  if (stoptime.type === "arrival" && stoptime.arrival_time) {
    const arrivalTime = fromGTFSTime(stoptime.arrival_time);
    stoptime.formatted_time = arrivalTime.format(config.timeFormat);
    stoptime.classes.push(arrivalTime.format("a"));
  } else if (stoptime.type === "departure" && stoptime.departure_time) {
    const departureTime = fromGTFSTime(stoptime.departure_time);
    stoptime.formatted_time = departureTime.format(config.timeFormat);
    stoptime.classes.push(departureTime.format("a"));
  }
  if (stoptime.pickup_type === 1) {
    stoptime.noPickup = true;
    stoptime.classes.push("no-pickup");
    if (timetable.noPickupSymbol !== null) {
      timetable.noPickupSymbolUsed = true;
    }
  } else if (stoptime.pickup_type === 2 || stoptime.pickup_type === 3) {
    stoptime.requestPickup = true;
    stoptime.classes.push("request-pickup");
    if (timetable.requestPickupSymbol !== null) {
      timetable.requestPickupSymbolUsed = true;
    }
  }
  if (stoptime.drop_off_type === 1) {
    stoptime.noDropoff = true;
    stoptime.classes.push("no-drop-off");
    if (timetable.noDropoffSymbol !== null) {
      timetable.noDropoffSymbolUsed = true;
    }
  } else if (stoptime.drop_off_type === 2 || stoptime.drop_off_type === 3) {
    stoptime.requestDropoff = true;
    stoptime.classes.push("request-drop-off");
    if (timetable.requestDropoffSymbol !== null) {
      timetable.requestDropoffSymbolUsed = true;
    }
  }
  if (stoptime.timepoint === 0 || stoptime.departure_time === "") {
    stoptime.interpolated = true;
    stoptime.classes.push("interpolated");
    if (timetable.interpolatedStopSymbol !== null) {
      timetable.interpolatedStopSymbolUsed = true;
    }
  }
  if (stoptime.timepoint === null && stoptime.departure_time === null && stoptime.stop_sequence === null) {
    stoptime.skipped = true;
    stoptime.classes.push("skipped");
    if (timetable.noServiceSymbol !== null) {
      timetable.noServiceSymbolUsed = true;
    }
  }
  if (stoptime.timepoint === 1) {
    stoptime.classes.push("timepoint");
  }
  return stoptime;
}
function filterHourlyTimes(stops) {
  const firstStopTimes = [];
  const firstTripMinutes = minutesAfterMidnight(stops[0].trips[0].arrival_time);
  for (const trip of stops[0].trips) {
    const minutes = minutesAfterMidnight(trip.arrival_time);
    if (minutes >= firstTripMinutes + 60) {
      break;
    }
    firstStopTimes.push(fromGTFSTime(trip.arrival_time));
  }
  const firstStopTimesAndIndex = firstStopTimes.map((time, idx) => ({
    idx,
    time
  }));
  const sortedFirstStopTimesAndIndex = sortBy2(
    firstStopTimesAndIndex,
    (item) => Number.parseInt(item.time.format("m"), 10)
  );
  return stops.map((stop) => {
    stop.hourlyTimes = sortedFirstStopTimesAndIndex.map(
      (item) => fromGTFSTime(stop.trips[item.idx].arrival_time).format(":mm")
    );
    return stop;
  });
}
var days = [
  "monday",
  "tuesday",
  "wednesday",
  "thursday",
  "friday",
  "saturday",
  "sunday"
];
function formatDays(calendar, config) {
  const daysShort = config.daysShortStrings;
  let daysInARow = 0;
  let dayString = "";
  if (!calendar) {
    return "";
  }
  for (let i = 0; i <= 6; i += 1) {
    const currentDayOperating = calendar[days[i]] === 1;
    const previousDayOperating = i > 0 ? calendar[days[i - 1]] === 1 : false;
    const nextDayOperating = i < 6 ? calendar[days[i + 1]] === 1 : false;
    if (currentDayOperating) {
      if (dayString.length > 0) {
        if (!previousDayOperating) {
          dayString += ", ";
        } else if (daysInARow === 1) {
          dayString += "-";
        }
      }
      daysInARow += 1;
      if (dayString.length === 0 || !nextDayOperating || i === 6 || !previousDayOperating) {
        dayString += daysShort[i];
      }
    } else {
      daysInARow = 0;
    }
  }
  if (dayString.length === 0) {
    dayString = config.noRegularServiceDaysText;
  }
  return dayString;
}
function formatDaysLong(dayList, config) {
  const mapObject = zipObject(config.daysShortStrings, config.daysStrings);
  return replaceAll(dayList, mapObject);
}
function formatTrip(trip, timetable, calendars, config) {
  trip.calendar = find2(calendars, {
    service_id: trip.service_id
  });
  trip.dayList = formatDays(trip.calendar, config);
  trip.dayListLong = formatDaysLong(trip.dayList, config);
  if (timetable.routes.length === 1) {
    trip.route_short_name = timetable.routes[0].route_short_name;
  } else {
    const route = timetable.routes.find(
      (route2) => route2.route_id === trip.route_id
    );
    trip.route_short_name = route.route_short_name;
  }
  return trip;
}
function formatFrequency(frequency, config) {
  const startTime = fromGTFSTime(frequency.start_time);
  const endTime = fromGTFSTime(frequency.end_time);
  const headway = moment3.duration(frequency.headway_secs, "seconds");
  frequency.start_formatted_time = startTime.format(config.timeFormat);
  frequency.end_formatted_time = endTime.format(config.timeFormat);
  frequency.headway_min = Math.round(headway.asMinutes());
  return frequency;
}
function formatTimetableId({
  routeIds,
  directionId,
  days: days2,
  dates
}) {
  let timetableId = routeIds.join("_");
  if (calendarToCalendarCode(days2)) {
    timetableId += `|${calendarToCalendarCode(days2)}`;
  } else if (dates && dates.length > 0) {
    timetableId += `|${dates.join("_")}`;
  }
  if (!isNullOrEmpty(directionId)) {
    timetableId += `|${directionId}`;
  }
  return timetableId;
}
function createEmptyStoptime(stopId, tripId) {
  return {
    id: null,
    trip_id: tripId,
    arrival_time: null,
    departure_time: null,
    stop_id: stopId,
    stop_sequence: null,
    stop_headsign: null,
    pickup_type: null,
    drop_off_type: null,
    continuous_pickup: null,
    continuous_drop_off: null,
    shape_dist_traveled: null,
    timepoint: null
  };
}
function formatStops(timetable, config) {
  for (const trip of timetable.orderedTrips) {
    let stopIndex = -1;
    for (const [idx, stoptime] of trip.stoptimes.entries()) {
      const stop = find2(timetable.stops, (st, idx2) => {
        if (st.stop_id === stoptime.stop_id && idx2 > stopIndex) {
          stopIndex = idx2;
          return true;
        }
        return false;
      });
      if (!stop) {
        continue;
      }
      if (idx === 0) {
        stoptime.drop_off_type = 0;
      }
      if (idx === trip.stoptimes.length - 1) {
        stoptime.pickup_type = 0;
      }
      if (stop.type === "arrival" && idx < trip.stoptimes.length - 1) {
        const departureStoptime = clone(stoptime);
        departureStoptime.type = "departure";
        timetable.stops[stopIndex + 1].trips.push(
          formatStopTime(departureStoptime, timetable, config)
        );
      }
      if (!(stop.type === "arrival" && idx === 0)) {
        stoptime.type = "arrival";
        stop.trips.push(formatStopTime(stoptime, timetable, config));
      }
    }
    for (const stop of timetable.stops) {
      const lastStopTime = last2(stop.trips);
      if (!lastStopTime || lastStopTime.trip_id !== trip.trip_id) {
        stop.trips.push(
          formatStopTime(
            createEmptyStoptime(stop.stop_id, trip.trip_id),
            timetable,
            config
          )
        );
      }
    }
  }
  if (timetable.orientation === "hourly") {
    timetable.stops = filterHourlyTimes(timetable.stops);
  }
  for (const stop of timetable.stops) {
    stop.is_timepoint = stop.trips.some((stoptime) => isTimepoint(stoptime));
  }
  return timetable.stops;
}
function formatStopName(stop) {
  return `${stop.stop_name}${stop.type === "arrival" ? " (Arrival)" : stop.type === "departure" ? " (Departure)" : ""}`;
}
function formatTripContinuesFrom(trip) {
  return trip.continues_from_route ? trip.continues_from_route.route.route_short_name : "";
}
function formatTripContinuesAs(trip) {
  return trip.continues_as_route ? trip.continues_as_route.route.route_short_name : "";
}
function resetStoptimesToMidnight(trip) {
  const offsetSeconds = secondsAfterMidnight(
    first2(trip.stoptimes).departure_time
  );
  if (offsetSeconds > 0) {
    for (const stoptime of trip.stoptimes) {
      stoptime.departure_time = toGTFSTime(
        fromGTFSTime(stoptime.departure_time).subtract(
          offsetSeconds,
          "seconds"
        )
      );
      stoptime.arrival_time = toGTFSTime(
        fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, "seconds")
      );
    }
  }
  return trip;
}
function updateStoptimesByOffset(trip, offsetSeconds) {
  return trip.stoptimes.map((stoptime) => {
    delete stoptime._id;
    stoptime.departure_time = updateTimeByOffset(
      stoptime.departure_time,
      offsetSeconds
    );
    stoptime.arrival_time = updateTimeByOffset(
      stoptime.arrival_time,
      offsetSeconds
    );
    stoptime.trip_id = trip.trip_id;
    return stoptime;
  });
}
function formatRouteColor(route) {
  return route.route_color ? `#${route.route_color}` : "#000000";
}
function formatRouteTextColor(route) {
  return route.route_text_color ? `#${route.route_text_color}` : "#FFFFFF";
}
function formatTimetableLabel(timetable) {
  if (!isNullOrEmpty(timetable.timetable_label)) {
    return timetable.timetable_label;
  }
  let timetableLabel = "";
  if (timetable.routes && timetable.routes.length > 0) {
    timetableLabel += "Route ";
    if (!isNullOrEmpty(timetable.routes[0].route_short_name)) {
      timetableLabel += timetable.routes[0].route_short_name;
    } else if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
      timetableLabel += timetable.routes[0].route_long_name;
    }
  }
  if (timetable.stops && timetable.stops.length > 0) {
    const firstStop = timetable.stops[0].stop_name;
    const lastStop = timetable.stops[timetable.stops.length - 1].stop_name;
    if (firstStop === lastStop) {
      if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
        timetableLabel += ` - ${timetable.routes[0].route_long_name}`;
      }
      timetableLabel += " - Loop";
    } else {
      timetableLabel += ` - ${firstStop} to ${lastStop}`;
    }
  } else if (timetable.direction_name !== null) {
    timetableLabel += ` to ${timetable.direction_name}`;
  }
  return timetableLabel;
}
var formatRouteName = (route) => {
  if (route.route_long_name === null || route.route_long_name === "") {
    return `Route ${route.route_short_name}`;
  }
  return route.route_long_name ?? "Unknown";
};
var formatRouteNameForFilename = (route) => {
  if (route.route_short_name) {
    return route.route_short_name.replace(/\s/g, "-");
  } else if (route.route_long_name) {
    return route.route_long_name.replace(/\s/g, "-");
  }
  return "Unknown";
};
var formatListForDisplay = (list) => {
  return new Intl.ListFormat("en-US", {
    style: "long",
    type: "conjunction"
  }).format(list);
};
function mergeTimetablesWithSameId(timetables) {
  if (timetables.length === 0) {
    return [];
  }
  const mergedTimetables = groupBy2(timetables, "timetable_id");
  return Object.values(mergedTimetables).map((timetableGroup) => {
    const mergedTimetable = omit(timetableGroup[0], "route_id");
    mergedTimetable.route_ids = timetableGroup.map(
      (timetable) => timetable.route_id
    );
    return mergedTimetable;
  });
}

// src/lib/file-utils.ts
var homeDirectory = homedir();
async function getConfig(argv2) {
  let data;
  let config;
  try {
    data = await readFile(resolve(untildify(argv2.configPath)), "utf8");
  } catch (error) {
    throw new Error(
      `Cannot find configuration file at \`${argv2.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`
    );
  }
  try {
    config = JSON.parse(data);
  } catch (error) {
    throw new Error(
      `Cannot parse configuration file at \`${argv2.configPath}\`. Check to ensure that it is valid JSON.`
    );
  }
  if (argv2.skipImport === true) {
    config.skipImport = argv2.skipImport;
  }
  if (argv2.showOnlyTimepoint === true) {
    config.showOnlyTimepoint = argv2.showOnlyTimepoint;
  }
  return config;
}
function getPathToThisModuleFolder() {
  const __dirname = dirname(fileURLToPath(import.meta.url));
  let distFolderPath;
  if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
    distFolderPath = resolve(__dirname, "../../");
  } else if (__dirname.endsWith("/dist")) {
    distFolderPath = resolve(__dirname, "../");
  } else {
    distFolderPath = resolve(__dirname, "../../");
  }
  return distFolderPath;
}
function getPathToViewsFolder(config) {
  if (config.templatePath) {
    return untildify(config.templatePath);
  }
  return join(getPathToThisModuleFolder(), "views/default");
}
function getPathToTemplateFile(templateFileName, config) {
  const fullTemplateFileName = config.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
  return join(getPathToViewsFolder(config), fullTemplateFileName);
}
async function prepDirectory(outputPath, config) {
  try {
    await access(outputPath);
  } catch (error) {
    try {
      await mkdir(outputPath, { recursive: true });
    } catch (error2) {
      if (error2?.code === "ENOENT") {
        throw new Error(
          `Unable to write to ${outputPath}. Try running this command from a writable directory.`
        );
      }
      throw error2;
    }
  }
  const files = await readdir(outputPath);
  if (config.overwriteExistingFiles === false && files.length > 0) {
    throw new Error(
      `Output directory ${outputPath} is not empty. Please specify an empty directory.`
    );
  }
  if (config.overwriteExistingFiles === true) {
    await rm(join(outputPath, "*"), { recursive: true, force: true });
  }
}
async function copyStaticAssets(config, outputPath) {
  const viewsFolderPath = getPathToViewsFolder(config);
  const thisModuleFolderPath = getPathToThisModuleFolder();
  const foldersToCopy = ["css", "js", "img"];
  for (const folder of foldersToCopy) {
    if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
      await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
        recursive: true
      });
    }
  }
  if (config.hasGtfsRealtimeVehiclePositions || config.hasGtfsRealtimeTripUpdates || config.hasGtfsRealtimeAlerts) {
    await copyFile(
      join(thisModuleFolderPath, "dist/frontend_libraries/pbf.js"),
      join(outputPath, "js/pbf.js")
    );
    await copyFile(
      join(
        thisModuleFolderPath,
        "dist/frontend_libraries/gtfs-realtime.browser.proto.js"
      ),
      join(outputPath, "js/gtfs-realtime.browser.proto.js")
    );
  }
  if (config.hasGtfsRealtimeAlerts) {
    await copyFile(
      join(thisModuleFolderPath, "dist/frontend_libraries/anchorme.min.js"),
      join(outputPath, "js/anchorme.min.js")
    );
  }
  if (config.showMap) {
    await copyFile(
      join(thisModuleFolderPath, "dist/frontend_libraries/maplibre-gl.js"),
      join(outputPath, "js/maplibre-gl.js")
    );
    await copyFile(
      join(thisModuleFolderPath, "dist/frontend_libraries/maplibre-gl.css"),
      join(outputPath, "css/maplibre-gl.css")
    );
    await copyFile(
      join(
        thisModuleFolderPath,
        "dist/frontend_libraries/maplibre-gl-geocoder.js"
      ),
      join(outputPath, "js/maplibre-gl-geocoder.js")
    );
    await copyFile(
      join(
        thisModuleFolderPath,
        "dist/frontend_libraries/maplibre-gl-geocoder.css"
      ),
      join(outputPath, "css/maplibre-gl-geocoder.css")
    );
  }
}
function zipFolder(outputPath) {
  const output = createWriteStream(join(outputPath, "timetables.zip"));
  const archive = archiver("zip");
  return new Promise((resolve2, reject) => {
    output.on("close", resolve2);
    archive.on("error", reject);
    archive.pipe(output);
    archive.glob("**/*.{txt,css,js,png,jpg,jpeg,svg,csv,pdf,html}", {
      cwd: outputPath
    });
    archive.finalize();
  });
}
function generateTimetablePageFileName(timetablePage, config) {
  if (timetablePage.filename) {
    return sanitize(timetablePage.filename);
  }
  if (config.groupTimetablesIntoPages === true && uniqBy2(timetablePage.timetables, "route_id").length === 1) {
    const route = timetablePage.timetables[0].routes[0];
    return sanitize(`${formatRouteNameForFilename(route).toLowerCase()}.html`);
  }
  const timetable = timetablePage.timetables[0];
  if (timetable.timetable_id) {
    return sanitize(
      `${timetable.timetable_id.replace(/\|/g, "_").toLowerCase()}.html`
    );
  }
  let filename = "";
  for (const route of timetable.routes) {
    filename += `_${formatRouteNameForFilename(route)}`;
  }
  if (!isNullOrEmpty(timetable.direction_id)) {
    filename += `_${timetable.direction_id}`;
  }
  filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.html`;
  return sanitize(filename.toLowerCase());
}
function generateCSVFileName(timetable, config) {
  let filename = timetable.timetable_id ?? "";
  for (const route of timetable.routes) {
    filename += `_${formatRouteNameForFilename(route)}`;
  }
  if (!isNullOrEmpty(timetable.direction_id)) {
    filename += `_${timetable.direction_id}`;
  }
  filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.csv`;
  return sanitize(filename).toLowerCase();
}
function generateFolderName(timetablePage) {
  const timetable = timetablePage.consolidatedTimetables[0];
  if (!timetable.start_date || !timetable.end_date) {
    return "timetables";
  }
  return sanitize(`${timetable.start_date}-${timetable.end_date}`);
}
async function renderTemplate(templateFileName, templateVars, config) {
  const templatePath = getPathToTemplateFile(templateFileName, config);
  const html = await renderFile(templatePath, {
    _,
    cssEscape,
    md: (text) => sanitizeHtml(marked.parseInline(text)),
    ...template_functions_exports,
    formatRouteColor,
    formatRouteTextColor,
    ...templateVars
  });
  if (config.beautify === true) {
    return beautify.html_beautify(html, {
      indent_size: 2
    });
  }
  return html;
}
async function renderPdf(htmlPath) {
  const pdfPath = htmlPath.replace(/html$/, "pdf");
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.emulateMediaType("print");
  await page.goto(`file://${htmlPath}`, {
    waitUntil: "networkidle0"
  });
  await page.pdf({
    path: pdfPath
  });
  await browser.close();
}
function untildify(pathWithTilde) {
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
}

// src/lib/gtfs-to-html.ts
import path from "path";
import { mkdir as mkdir2, writeFile } from "fs/promises";
import { openDb as openDb2, importGtfs } from "gtfs";
import sanitize2 from "sanitize-filename";
var gtfsToHtml = async (initialConfig) => {
  const config = setDefaultConfig(initialConfig);
  const startTime = process.hrtime.bigint();
  const agencyKey = config.agencies.map(
    (agency) => agency.agencyKey ?? agency.agency_key ?? "unknown"
  ).join("-");
  const outputPath = config.outputPath ? untildify(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
  await prepDirectory(outputPath, config);
  try {
    openDb2(config);
  } catch (error) {
    if (error?.code === "SQLITE_CANTOPEN") {
      logError(config)(
        `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
      );
    }
    throw error;
  }
  if (!config.agencies || config.agencies.length === 0) {
    throw new Error("No agencies defined in `config.json`");
  }
  if (!config.skipImport) {
    await importGtfs(config);
  }
  const stats = {
    timetables: 0,
    timetablePages: 0,
    calendars: 0,
    routes: 0,
    trips: 0,
    stops: 0,
    warnings: []
  };
  const timetablePages = [];
  const timetablePageIds = getTimetablePagesForAgency(config).map(
    (timetablePage) => timetablePage.timetable_page_id
  );
  if (config.noHead !== true && ["html", "pdf"].includes(config.outputFormat)) {
    await copyStaticAssets(config, outputPath);
  }
  const bar = progressBar(
    `${agencyKey}: Generating ${config.outputFormat.toUpperCase()} timetables {bar} {value}/{total}`,
    timetablePageIds.length,
    config
  );
  for (const timetablePageId of timetablePageIds) {
    try {
      const timetablePage = await getFormattedTimetablePage(
        timetablePageId,
        config
      );
      for (const timetable of timetablePage.consolidatedTimetables) {
        if (timetable.warnings) {
          for (const warning of timetable.warnings) {
            stats.warnings.push(warning);
            bar?.interrupt(warning);
          }
        }
      }
      if (timetablePage.consolidatedTimetables.length === 0) {
        throw new Error(
          `No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
        );
      }
      stats.timetables += timetablePage.consolidatedTimetables.length;
      stats.timetablePages += 1;
      const datePath = generateFolderName(timetablePage);
      await mkdir2(path.join(outputPath, datePath), { recursive: true });
      config.assetPath = "../";
      timetablePage.relativePath = path.join(
        datePath,
        sanitize2(timetablePage.filename)
      );
      if (config.outputFormat === "csv") {
        for (const timetable of timetablePage.consolidatedTimetables) {
          const csv = await generateTimetableCSV(timetable);
          const csvPath = path.join(
            outputPath,
            datePath,
            generateCSVFileName(timetable, config)
          );
          await writeFile(csvPath, csv);
        }
      } else {
        const html = await generateTimetableHTML(timetablePage, config);
        const htmlPath = path.join(
          outputPath,
          datePath,
          sanitize2(timetablePage.filename)
        );
        await writeFile(htmlPath, html);
        if (config.outputFormat === "pdf") {
          await renderPdf(htmlPath);
        }
      }
      timetablePages.push(timetablePage);
      const timetableStats = generateStats(timetablePage);
      stats.stops += timetableStats.stops;
      stats.routes += timetableStats.routes;
      stats.trips += timetableStats.trips;
      stats.calendars += timetableStats.calendars;
    } catch (error) {
      stats.warnings.push(error?.message);
      bar?.interrupt(error.message);
    }
    bar?.increment();
  }
  if (config.outputFormat === "html") {
    config.assetPath = "";
    const html = await generateOverviewHTML(timetablePages, config);
    await writeFile(path.join(outputPath, "index.html"), html);
  }
  const logText = generateLogText(stats, config);
  await writeFile(path.join(outputPath, "log.txt"), logText);
  if (config.zipOutput) {
    await zipFolder(outputPath);
  }
  const fullOutputPath = path.join(
    outputPath,
    config.zipOutput ? "/timetables.zip" : ""
  );
  log(config)(
    `${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullOutputPath}`
  );
  logStats(config)(stats);
  const endTime = process.hrtime.bigint();
  const elapsedSeconds = Number(endTime - startTime) / 1e9;
  log(config)(
    `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${elapsedSeconds.toFixed(1)} seconds`
  );
  return fullOutputPath;
};
var gtfs_to_html_default = gtfsToHtml;

// src/bin/gtfs-to-html.ts
var pe = new PrettyError();
var { argv } = yargs(hideBin(process.argv)).usage("Usage: $0 --configPath ./config.json").help().option("c", {
  alias: "configPath",
  describe: "Path to config file",
  default: "./config.json",
  type: "string"
}).option("s", {
  alias: "skipImport",
  describe: "Don\u2019t import GTFS file.",
  type: "boolean"
}).default("skipImport", void 0).option("t", {
  alias: "showOnlyTimepoint",
  describe: "Show only stops with a `timepoint` value in `stops.txt`",
  type: "boolean"
}).default("showOnlyTimepoint", void 0);
var handleError = (error) => {
  const text = error || "Unknown Error";
  process.stdout.write(`
${formatError(text)}
`);
  console.error(pe.render(error));
  process.exit(1);
};
var setupImport = async () => {
  const config = await getConfig(argv);
  await gtfs_to_html_default(config);
  process.exit();
};
setupImport().catch(handleError);
//# sourceMappingURL=gtfs-to-html.js.map