Transit Service With Roadbook

Demonstrate building a roadbook or itinerary using the Transit Service to get directions for public transport mode.

  1. Example
  2. Running the Sample Locally

Example

Transit Service With Roadbook
        let map: woosmap.map.Map;
let transitService: woosmap.map.TransitService;
let transitRequest: woosmap.map.transit.TransitRouteRequest;
let markersArray: woosmap.map.Marker[] = [];
let originContainer: HTMLElement;
let destinationContainer: HTMLElement;
let transitRenderer: woosmap.map.TransitRenderer;

function createMarker(
  position: woosmap.map.LatLngLiteral | woosmap.map.LatLng,
  label: string,
  url: string,
): woosmap.map.Marker {
  return new woosmap.map.Marker({
    map,
    position,
    draggable: true,
    icon: {
      url,
      labelOrigin: new woosmap.map.Point(13, 15),
      scaledSize: {
        height: 38,
        width: 26,
      },
    },
    label: {
      text: label,
      color: "white",
    },
  });
}

function clearMarkers(): void {
  for (const marker of markersArray) {
    marker.setMap(null);
  }
  markersArray = [];
}

function displayTransitMarkers(): void {
  clearMarkers();
  const originMarker = createMarker(
    transitRequest.origin,
    "O",
    "https://images.woosmap.com/marker-blue.svg",
  );
  originMarker.addListener("dragend", () => {
    transitRequest.origin = originMarker.getPosition();
    setLatLngToContainer(originContainer, originMarker.getPosition().toJSON());
    calculateTransit();
  });
  const destinationMarker = createMarker(
    transitRequest.destination,
    "D",
    "https://images.woosmap.com/marker-red.svg",
  );
  destinationMarker.addListener("dragend", () => {
    transitRequest.destination = destinationMarker.getPosition();
    setLatLngToContainer(
      destinationContainer,
      destinationMarker.getPosition().toJSON(),
    );
    calculateTransit();
  });
  markersArray.push(originMarker);
  markersArray.push(destinationMarker);
}

function createDefaultRequest(): woosmap.map.transit.TransitRouteRequest {
  const origin = { lat: 51.6511, lng: -0.1615 };
  const destination = { lat: 51.5146, lng: -0.0212 };
  setLatLngToContainer(originContainer, origin);
  setLatLngToContainer(destinationContainer, destination);

  return {
    origin,
    destination,
  };
}

function setLatLngToContainer(
  element: HTMLElement,
  location: woosmap.map.LatLngLiteral,
): void {
  element.innerHTML = `<li><span>lat: ${location.lat.toFixed(4)}, lng: ${location.lng.toFixed(4)}<span></li>`;
}

function createRoutesTable(response: woosmap.map.transit.TransitRouteResponse) {
  const directionTripElements = response.routes.map(
    (route: woosmap.map.transit.TransitRoute, index: number) => {
      const distanceTotal = route.legs.reduce(
        (total, leg) => total + leg.distance,
        0,
      );
      const directionTrip = document.createElement("div");
      directionTrip.className = "directionTrip";
      if (index === 0) {
        directionTrip.classList.add("directionTrip__selected");
      }

      const startName =
        route.legs.find(
          (leg: woosmap.map.transit.TransitLeg) =>
            leg.start_location && leg.start_location.name !== null,
        )?.start_location.name || "Walking";

      const travelModeIconSrc =
        startName === "Walking"
          ? "https://images.woosmap.com/directions/walk_black.png"
          : "https://images.woosmap.com/directions/transit_black.png";

      directionTrip.innerHTML = `
            <img class="directionTrip__travelModeIcon" src="${travelModeIconSrc}">
            <div class="directionTrip__description">
                <div class="directionTrip__numbers">
                    <div class="directionTrip__duration">${formatTime(route.duration)}</div>
                    <div class="directionTrip__distance">${formatDistance(distanceTotal)}</div>
                </div>
                <div class="directionTrip__title">through ${startName}</div>
                <div class="directionTrip__summary">${formatTime(route.duration)}</div>
                <div class="directionTrip__detailsMsg"></div>
            </div>
        `;
      directionTrip.append(getLegsSummaryHTML(route));
      directionTrip.addEventListener("click", () => {
        buildRoadBook(response.routes[index]);
        selectCorrectRoute(index);
        transitRenderer.setRouteIndex(index);
      });
      buildRoadBook(response.routes[0]);
      return directionTrip;
    },
  );
  function getLegsSummaryHTML(
    route: woosmap.map.transit.TransitRoute,
  ): HTMLDivElement {
    const $instructions: HTMLDivElement = document.createElement("div");
    $instructions.className = "directionTrip__summaryoverview";
    if (route.legs) {
      const $instructionsSteps: HTMLDivElement[] = route.legs.map(
        (leg, index) => {
          const $stepContainer = document.createElement("div");
          $stepContainer.className = "step-container";
          const $step: HTMLDivElement = document.createElement("div");
          $step.className = `${leg.travel_mode === `pedestrian` ? "step" : "pill"}`;
          $step.setAttribute(
            "style",
            `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`,
          );
          $step.innerHTML = `<span style="${
            leg.travel_mode === `pedestrian`
              ? "background-color: transparent"
              : `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`
          }" 
                               class="icon ${
                                 leg.travel_mode === `pedestrian`
                                   ? `pedestrian`
                                   : getTransitMode(leg.transport.mode)
                               }"> 
                                </span><span class="transitduration">${leg.travel_mode === `transit` ? (leg.transport.short_name ? leg.transport.short_name : ``) : ``}</span>
            ${
              leg.travel_mode === `pedestrian`
                ? `<span style="font-weight: bold">${formatTimeNumberOnly(leg.duration)}' ${index < route.legs.length - 1 ? "> " : ""}   </span>`
                : ``
            } 
            `;
          $stepContainer.appendChild($step);
          leg.travel_mode === `transit`
            ? $step.insertAdjacentHTML(
                "afterend",
                '<span style="font-weight: bold"> > </span>',
              )
            : "";
          return $stepContainer;
        },
      );
      $instructions.replaceChildren(...$instructionsSteps);
    }
    return $instructions;
  }

  function buildRoadBook(route: woosmap.map.transit.TransitRoute) {
    const $roadbookSection = document.getElementById(
      "roadbook__container",
    ) as HTMLElement;

    const $roadbookHeader: HTMLDivElement = document.createElement("div");
    $roadbookHeader.className = "trouteRoadbook__header";
    $roadbookHeader.innerHTML = `
                                <span class="trouteRoadbook__headerFromTo">
                                    <div class="trouteRoadbook__headerWaypoint"> From <span class="routeRoadbook__headerFrom">origin</span></div>
                                    <div class="trouteRoadbook__headerWaypoint"> To<span class="routeRoadbook__headerTo">destination</span></div>
                                </span>`;
    const $roadbookBody: HTMLDivElement = document.createElement("div");
    $roadbookBody.className = "trouteRoadbook__body";
    const $instructions: HTMLDivElement = document.createElement("div");
    $instructions.className = "trouteRoadbook__steps";
    route.legs.forEach((leg) => {
      const $leg: HTMLDivElement = document.createElement("div");
      $leg.className = "route";
      $leg.innerHTML = `
                <div style="${leg.travel_mode === `pedestrian` ? "background-color: #1aae1d" : `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`}" class="icon ${leg.travel_mode === `pedestrian` ? `pedestrian` : getTransitMode(leg.transport.mode)}"></div>
                <div class="details">
                    ${
                      leg.travel_mode === "pedestrian"
                        ? `<span>Walk</span><div class='subdetails' style='border-left: 3px dashed #1aae1d'>`
                        : `<span>Take the ${getTransitMode(leg.transport.mode)}</span>
                    <div class="subdetails" style="border-left: 3px solid ${leg.transport.color ? leg.transport.color : `#c0bfbf`}"><p><span class='line'>${leg.transport.name}</span> ${leg.transport.headsign}</p>`
                    }
                    ${
                      leg.travel_mode === "pedestrian"
                        ? "<span></span>"
                        : `<p class='line'>From ${leg.start_location.name} > To ${leg.end_location.name}</p>`
                    }
                   
                    <span class="duration">${formatTime(leg.duration)}</span>
                    <span class="time">${formatDistance(leg.distance)}</span>
                    <div class="divider"></div><div>
                </div>
                `;
      $instructions.appendChild($leg);
    });
    $roadbookBody.appendChild($instructions);
    $roadbookSection.replaceChildren($roadbookBody);
  }

  function formatDistance(meters: number): string {
    if (meters < 1000) {
      return `${meters} m`;
    } else {
      return `${(meters / 1000).toFixed(2)} km`;
    }
  }

  function formatTime(seconds: number): string {
    const minutes = Math.round(seconds / 60);
    if (minutes < 60) {
      return `${minutes}m`;
    } else {
      const hours = Math.floor(minutes / 60);
      const remainingMinutes = minutes % 60;
      return `${hours}h${remainingMinutes}m`;
    }
  }
  function formatTimeNumberOnly(seconds: number): number {
    const minutes = Math.round(seconds / 60);
    return minutes;
  }
  function getTransitMode(mode: string): string {
    const transitModes = {
      rail: [
        "highSpeedTrain",
        "intercityTrain",
        "interRegionalTrain",
        "regionalTrain",
        "cityTrain",
        "subway",
        "monorail",
        "inclined",
      ],
      tram: ["lightRail"],
      bus: ["bus", "privateBus", "busRapid"],
      ferries: ["ferry"],
      aerial: ["aerial", "flight", "spaceship"],
    };
    for (const key in transitModes) {
      if (transitModes[key].includes(mode)) {
        return key;
      }
    }
    return "pedestrian";
  }

  function selectCorrectRoute(index: number) {
    document
      .querySelectorAll(".directionTrip__selected")
      .forEach((selectedElement) => {
        selectedElement.classList.remove("directionTrip__selected");
      });
    directionTripElements[index].classList.add("directionTrip__selected");
  }

  const tableContainer = document.querySelector(
    ".tableContainer",
  ) as HTMLElement;
  tableContainer.innerHTML = "";
  directionTripElements.forEach((element) =>
    tableContainer.appendChild(element),
  );
  tableContainer.style.display = "block";
}

function displayOrHideError(error: string, status: string) {
  const infoMsgElement = document.getElementById("infoMessage") as HTMLElement;
  const $roadbookSection = document.getElementById(
    "roadbook__container",
  ) as HTMLElement;
  if (error === "") {
    infoMsgElement.innerText = "Drag markers to update route";
  } else {
    infoMsgElement.innerHTML = error;
    infoMsgElement.style.display = "block";
    const tableContainer = document.querySelector(
      ".tableContainer",
    ) as HTMLElement;
    tableContainer.innerHTML = "";
    tableContainer.style.display = "none";
    $roadbookSection.innerHTML = `Error calculating transit route:", ${error}`;
  }
  if (status !== "OK") {
    $roadbookSection.innerHTML = "No route found";
  }
}

function toggleProgress() {
  (document.querySelector(".linear-progress") as HTMLElement).classList.toggle(
    "hide",
  );
}

function displayTransitRoute(routes: woosmap.map.transit.TransitRoute[]) {
  transitRenderer.setRoutes(routes);
}

function calculateTransit(): void {
  toggleProgress();

  transitService
    .route(transitRequest)
    .then(handleResponse)
    .catch((error) => {
      console.error("Error calculating transit route:", error);
      displayOrHideError(error, "");
      toggleProgress();
    });
}

function handleResponse(response: woosmap.map.transit.TransitRouteResponse) {
  displayTransitRoute(response.routes);
  displayTransitMarkers();
  createRoutesTable(response);
  displayOrHideError("", response.status);
  toggleProgress();
}

function isValidDate(date: Date): boolean {
  return date.getTime && typeof date.getTime === "function";
}

function updateTime(timeType: "departureTime" | "arrivalTime"): void {
  const timeElement = document.getElementById(
    `${timeType}-time`,
  ) as HTMLInputElement;
  const datetimeRadioButton = document.getElementById(
    "datetime",
  ) as HTMLInputElement;
  const otherTimeType =
    timeType === "departureTime" ? "arrivalTime" : "departureTime";
  const otherTimeElement = document.getElementById(
    `${otherTimeType}-time`,
  ) as HTMLInputElement;

  if (!timeElement || !otherTimeElement) {
    return;
  }
  timeElement.min = new Date().toISOString().slice(0, 16);
  timeElement.disabled = true;
  document.querySelectorAll(`input[name="${timeType}"]`).forEach((el) => {
    el.addEventListener("change", () => {
      const selectedOption = (el as HTMLInputElement).value;
      switch (selectedOption) {
        case "empty":
          delete transitRequest[timeType];
          timeElement.disabled = true;
          break;
        case "now":
        case "datetime":
          delete transitRequest[otherTimeType];
          (
            document.querySelector(
              `input[name="${otherTimeType}"][value="empty"]`,
            ) as HTMLInputElement
          ).checked = true;
          otherTimeElement.value = "";
          otherTimeElement.disabled = true;
          if (timeElement.value) {
            const newDate = new Date(timeElement.value);
            transitRequest[timeType] = isValidDate(newDate)
              ? newDate.getTime().toString()
              : undefined;
          } else {
            transitRequest[timeType] = undefined;
          }
          timeElement.disabled = false;
          break;
      }
      calculateTransit();
    });
  });

  timeElement.addEventListener("change", () => {
    if (datetimeRadioButton) {
      datetimeRadioButton.checked = true;
    }
    const newDate = new Date(timeElement.value);
    transitRequest[timeType] = isValidDate(newDate)
      ? newDate.getTime().toString()
      : undefined;
    calculateTransit();
  });
}

function registerAddButton(
  selector: string,
  element: HTMLElement,
  loc: woosmap.map.LatLngLiteral[] | woosmap.map.LatLngLiteral,
): void {
  const button = document.querySelector(selector) as HTMLElement;
  button.addEventListener("click", () => {
    if (button.classList.contains("addLocation__selected")) {
      button.classList.remove("addLocation__selected");
      document.getElementById("map")?.classList.remove("cursor-crosshair");
      woosmap.map.event.clearListeners(map, "click");
      return;
    }
    button.classList.add("addLocation__selected");
    document.getElementById("map")?.classList.add("cursor-crosshair");
    woosmap.map.event.addListenerOnce(map, "click", (e) => {
      document.getElementById("map")?.classList.remove("cursor-crosshair");
      button.classList.remove("addLocation__selected");
      const location = e.latlng;
      if (element === originContainer) {
        transitRequest.origin = location;
      }
      if (element === destinationContainer) {
        transitRequest.destination = location;
      }
      setLatLngToContainer(element, location);
      calculateTransit();
    });
  });
}

const includedModes: string[] = [];
const excludedModes: string[] = [];
const modes = {
  train: [
    "highSpeedTrain",
    "intercityTrain",
    "interRegionalTrain",
    "regionalTrain",
    "cityTrain",
    "subway",
    "lightRail",
    "monorail",
    "inclined",
  ],
  bus: ["bus", "privateBus", "busRapid"],
  ferry: ["ferry"],
  aerial: ["aerial", "flight", "spaceship"],
};

function updateTransitOptions() {
  const checkboxes = document.querySelectorAll('.modesChk[type="checkbox"]');
  const includeRadio = document.querySelector(
    "#includeModes",
  ) as HTMLInputElement;
  const excludeRadio = document.querySelector(
    "#excludeModes",
  ) as HTMLInputElement;

  function updateMode() {
    includedModes.length = 0;
    excludedModes.length = 0;

    Object.keys(modes).forEach((mode) => {
      const checkbox = document.querySelector(
        `#mode${mode.charAt(0).toUpperCase() + mode.slice(1)}`,
      ) as HTMLInputElement;
      if (checkbox.checked) {
        modes[mode].forEach((subMode) => {
          if (includeRadio.checked) {
            includedModes.push(subMode);
          } else if (excludeRadio.checked) {
            excludedModes.push("-" + subMode);
          }
        });
      }
    });

    if (includedModes.length === 0 && excludedModes.length === 0) {
      delete transitRequest.modes;
    } else {
      transitRequest.modes = includeRadio.checked
        ? includedModes
        : excludedModes;
    }
    calculateTransit();
  }

  checkboxes.forEach((checkbox) => {
    checkbox.addEventListener("change", updateMode);
  });

  includeRadio.addEventListener("change", updateMode);
  excludeRadio.addEventListener("change", updateMode);
}

function initUI(): void {
  updateTime("departureTime");
  updateTime("arrivalTime");
  updateTransitOptions();
  registerAddButton(
    ".addLocation__destinations",
    destinationContainer,
    transitRequest.destination as woosmap.map.LatLngLiteral,
  );
  registerAddButton(
    ".addLocation__origins",
    originContainer,
    transitRequest.origin as woosmap.map.LatLngLiteral,
  );
}

function initMap(): void {
  map = new woosmap.map.Map(document.getElementById("map") as HTMLElement, {
    center: { lat: 51.5074, lng: -0.1478 },
    zoom: 10,
  });
  transitService = new woosmap.map.TransitService();
  transitRenderer = new woosmap.map.TransitRenderer({});
  transitRenderer.setMap(map);
  originContainer = document.getElementById("origin") as HTMLElement;
  destinationContainer = document.getElementById("destination") as HTMLElement;
  transitRequest = createDefaultRequest();
  initUI();
  calculateTransit();
}

declare global {
  interface Window {
    initMap: () => void;
  }
}
window.initMap = initMap;

    
        let map;
let transitService;
let transitRequest;
let markersArray = [];
let originContainer;
let destinationContainer;
let transitRenderer;

function createMarker(position, label, url) {
  return new woosmap.map.Marker({
    map,
    position,
    draggable: true,
    icon: {
      url,
      labelOrigin: new woosmap.map.Point(13, 15),
      scaledSize: {
        height: 38,
        width: 26,
      },
    },
    label: {
      text: label,
      color: "white",
    },
  });
}

function clearMarkers() {
  for (const marker of markersArray) {
    marker.setMap(null);
  }

  markersArray = [];
}

function displayTransitMarkers() {
  clearMarkers();

  const originMarker = createMarker(
    transitRequest.origin,
    "O",
    "https://images.woosmap.com/marker-blue.svg",
  );

  originMarker.addListener("dragend", () => {
    transitRequest.origin = originMarker.getPosition();
    setLatLngToContainer(originContainer, originMarker.getPosition().toJSON());
    calculateTransit();
  });

  const destinationMarker = createMarker(
    transitRequest.destination,
    "D",
    "https://images.woosmap.com/marker-red.svg",
  );

  destinationMarker.addListener("dragend", () => {
    transitRequest.destination = destinationMarker.getPosition();
    setLatLngToContainer(
      destinationContainer,
      destinationMarker.getPosition().toJSON(),
    );
    calculateTransit();
  });
  markersArray.push(originMarker);
  markersArray.push(destinationMarker);
}

function createDefaultRequest() {
  const origin = { lat: 51.6511, lng: -0.1615 };
  const destination = { lat: 51.5146, lng: -0.0212 };

  setLatLngToContainer(originContainer, origin);
  setLatLngToContainer(destinationContainer, destination);
  return {
    origin,
    destination,
  };
}

function setLatLngToContainer(element, location) {
  element.innerHTML = `<li><span>lat: ${location.lat.toFixed(4)}, lng: ${location.lng.toFixed(4)}<span></li>`;
}

function createRoutesTable(response) {
  const directionTripElements = response.routes.map((route, index) => {
    const distanceTotal = route.legs.reduce(
      (total, leg) => total + leg.distance,
      0,
    );
    const directionTrip = document.createElement("div");

    directionTrip.className = "directionTrip";
    if (index === 0) {
      directionTrip.classList.add("directionTrip__selected");
    }

    const startName =
      route.legs.find(
        (leg) => leg.start_location && leg.start_location.name !== null,
      )?.start_location.name || "Walking";
    const travelModeIconSrc =
      startName === "Walking"
        ? "https://images.woosmap.com/directions/walk_black.png"
        : "https://images.woosmap.com/directions/transit_black.png";

    directionTrip.innerHTML = `
            <img class="directionTrip__travelModeIcon" src="${travelModeIconSrc}">
            <div class="directionTrip__description">
                <div class="directionTrip__numbers">
                    <div class="directionTrip__duration">${formatTime(route.duration)}</div>
                    <div class="directionTrip__distance">${formatDistance(distanceTotal)}</div>
                </div>
                <div class="directionTrip__title">through ${startName}</div>
                <div class="directionTrip__summary">${formatTime(route.duration)}</div>
                <div class="directionTrip__detailsMsg"></div>
            </div>
        `;
    directionTrip.append(getLegsSummaryHTML(route));
    directionTrip.addEventListener("click", () => {
      buildRoadBook(response.routes[index]);
      selectCorrectRoute(index);
      transitRenderer.setRouteIndex(index);
    });
    buildRoadBook(response.routes[0]);
    return directionTrip;
  });

  function getLegsSummaryHTML(route) {
    const $instructions = document.createElement("div");

    $instructions.className = "directionTrip__summaryoverview";
    if (route.legs) {
      const $instructionsSteps = route.legs.map((leg, index) => {
        const $stepContainer = document.createElement("div");

        $stepContainer.className = "step-container";

        const $step = document.createElement("div");

        $step.className = `${leg.travel_mode === `pedestrian` ? "step" : "pill"}`;
        $step.setAttribute(
          "style",
          `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`,
        );
        $step.innerHTML = `<span style="${
          leg.travel_mode === `pedestrian`
            ? "background-color: transparent"
            : `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`
        }" 
                               class="icon ${
                                 leg.travel_mode === `pedestrian`
                                   ? `pedestrian`
                                   : getTransitMode(leg.transport.mode)
                               }"> 
                                </span><span class="transitduration">${leg.travel_mode === `transit` ? (leg.transport.short_name ? leg.transport.short_name : ``) : ``}</span>
            ${
              leg.travel_mode === `pedestrian`
                ? `<span style="font-weight: bold">${formatTimeNumberOnly(leg.duration)}' ${index < route.legs.length - 1 ? "> " : ""}   </span>`
                : ``
            } 
            `;
        $stepContainer.appendChild($step);
        leg.travel_mode === `transit`
          ? $step.insertAdjacentHTML(
              "afterend",
              '<span style="font-weight: bold"> > </span>',
            )
          : "";
        return $stepContainer;
      });

      $instructions.replaceChildren(...$instructionsSteps);
    }
    return $instructions;
  }

  function buildRoadBook(route) {
    const $roadbookSection = document.getElementById("roadbook__container");
    const $roadbookHeader = document.createElement("div");

    $roadbookHeader.className = "trouteRoadbook__header";
    $roadbookHeader.innerHTML = `
                                <span class="trouteRoadbook__headerFromTo">
                                    <div class="trouteRoadbook__headerWaypoint"> From <span class="routeRoadbook__headerFrom">origin</span></div>
                                    <div class="trouteRoadbook__headerWaypoint"> To<span class="routeRoadbook__headerTo">destination</span></div>
                                </span>`;

    const $roadbookBody = document.createElement("div");

    $roadbookBody.className = "trouteRoadbook__body";

    const $instructions = document.createElement("div");

    $instructions.className = "trouteRoadbook__steps";
    route.legs.forEach((leg) => {
      const $leg = document.createElement("div");

      $leg.className = "route";
      $leg.innerHTML = `
                <div style="${leg.travel_mode === `pedestrian` ? "background-color: #1aae1d" : `background-color: ${leg.transport.color ? leg.transport.color : `#c0bfbf`}`}" class="icon ${leg.travel_mode === `pedestrian` ? `pedestrian` : getTransitMode(leg.transport.mode)}"></div>
                <div class="details">
                    ${
                      leg.travel_mode === "pedestrian"
                        ? `<span>Walk</span><div class='subdetails' style='border-left: 3px dashed #1aae1d'>`
                        : `<span>Take the ${getTransitMode(leg.transport.mode)}</span>
                    <div class="subdetails" style="border-left: 3px solid ${leg.transport.color ? leg.transport.color : `#c0bfbf`}"><p><span class='line'>${leg.transport.name}</span> ${leg.transport.headsign}</p>`
                    }
                    ${
                      leg.travel_mode === "pedestrian"
                        ? "<span></span>"
                        : `<p class='line'>From ${leg.start_location.name} > To ${leg.end_location.name}</p>`
                    }
                   
                    <span class="duration">${formatTime(leg.duration)}</span>
                    <span class="time">${formatDistance(leg.distance)}</span>
                    <div class="divider"></div><div>
                </div>
                `;
      $instructions.appendChild($leg);
    });
    $roadbookBody.appendChild($instructions);
    $roadbookSection.replaceChildren($roadbookBody);
  }

  function formatDistance(meters) {
    if (meters < 1000) {
      return `${meters} m`;
    } else {
      return `${(meters / 1000).toFixed(2)} km`;
    }
  }

  function formatTime(seconds) {
    const minutes = Math.round(seconds / 60);

    if (minutes < 60) {
      return `${minutes}m`;
    } else {
      const hours = Math.floor(minutes / 60);
      const remainingMinutes = minutes % 60;
      return `${hours}h${remainingMinutes}m`;
    }
  }

  function formatTimeNumberOnly(seconds) {
    const minutes = Math.round(seconds / 60);
    return minutes;
  }

  function getTransitMode(mode) {
    const transitModes = {
      rail: [
        "highSpeedTrain",
        "intercityTrain",
        "interRegionalTrain",
        "regionalTrain",
        "cityTrain",
        "subway",
        "monorail",
        "inclined",
      ],
      tram: ["lightRail"],
      bus: ["bus", "privateBus", "busRapid"],
      ferries: ["ferry"],
      aerial: ["aerial", "flight", "spaceship"],
    };

    for (const key in transitModes) {
      if (transitModes[key].includes(mode)) {
        return key;
      }
    }
    return "pedestrian";
  }

  function selectCorrectRoute(index) {
    document
      .querySelectorAll(".directionTrip__selected")
      .forEach((selectedElement) => {
        selectedElement.classList.remove("directionTrip__selected");
      });
    directionTripElements[index].classList.add("directionTrip__selected");
  }

  const tableContainer = document.querySelector(".tableContainer");

  tableContainer.innerHTML = "";
  directionTripElements.forEach((element) =>
    tableContainer.appendChild(element),
  );
  tableContainer.style.display = "block";
}

function displayOrHideError(error, status) {
  const infoMsgElement = document.getElementById("infoMessage");
  const $roadbookSection = document.getElementById("roadbook__container");

  if (error === "") {
    infoMsgElement.innerText = "Drag markers to update route";
  } else {
    infoMsgElement.innerHTML = error;
    infoMsgElement.style.display = "block";

    const tableContainer = document.querySelector(".tableContainer");

    tableContainer.innerHTML = "";
    tableContainer.style.display = "none";
    $roadbookSection.innerHTML = `Error calculating transit route:", ${error}`;
  }

  if (status !== "OK") {
    $roadbookSection.innerHTML = "No route found";
  }
}

function toggleProgress() {
  document.querySelector(".linear-progress").classList.toggle("hide");
}

function displayTransitRoute(routes) {
  transitRenderer.setRoutes(routes);
}

function calculateTransit() {
  toggleProgress();
  transitService
    .route(transitRequest)
    .then(handleResponse)
    .catch((error) => {
      console.error("Error calculating transit route:", error);
      displayOrHideError(error, "");
      toggleProgress();
    });
}

function handleResponse(response) {
  displayTransitRoute(response.routes);
  displayTransitMarkers();
  createRoutesTable(response);
  displayOrHideError("", response.status);
  toggleProgress();
}

function isValidDate(date) {
  return date.getTime && typeof date.getTime === "function";
}

function updateTime(timeType) {
  const timeElement = document.getElementById(`${timeType}-time`);
  const datetimeRadioButton = document.getElementById("datetime");
  const otherTimeType =
    timeType === "departureTime" ? "arrivalTime" : "departureTime";
  const otherTimeElement = document.getElementById(`${otherTimeType}-time`);

  if (!timeElement || !otherTimeElement) {
    return;
  }

  timeElement.min = new Date().toISOString().slice(0, 16);
  timeElement.disabled = true;
  document.querySelectorAll(`input[name="${timeType}"]`).forEach((el) => {
    el.addEventListener("change", () => {
      const selectedOption = el.value;

      switch (selectedOption) {
        case "empty":
          delete transitRequest[timeType];
          timeElement.disabled = true;
          break;
        case "now":
        case "datetime":
          delete transitRequest[otherTimeType];
          document.querySelector(
            `input[name="${otherTimeType}"][value="empty"]`,
          ).checked = true;
          otherTimeElement.value = "";
          otherTimeElement.disabled = true;
          if (timeElement.value) {
            const newDate = new Date(timeElement.value);

            transitRequest[timeType] = isValidDate(newDate)
              ? newDate.getTime().toString()
              : undefined;
          } else {
            transitRequest[timeType] = undefined;
          }

          timeElement.disabled = false;
          break;
      }

      calculateTransit();
    });
  });
  timeElement.addEventListener("change", () => {
    if (datetimeRadioButton) {
      datetimeRadioButton.checked = true;
    }

    const newDate = new Date(timeElement.value);

    transitRequest[timeType] = isValidDate(newDate)
      ? newDate.getTime().toString()
      : undefined;
    calculateTransit();
  });
}

function registerAddButton(selector, element, loc) {
  const button = document.querySelector(selector);

  button.addEventListener("click", () => {
    if (button.classList.contains("addLocation__selected")) {
      button.classList.remove("addLocation__selected");
      document.getElementById("map")?.classList.remove("cursor-crosshair");
      woosmap.map.event.clearListeners(map, "click");
      return;
    }

    button.classList.add("addLocation__selected");
    document.getElementById("map")?.classList.add("cursor-crosshair");
    woosmap.map.event.addListenerOnce(map, "click", (e) => {
      document.getElementById("map")?.classList.remove("cursor-crosshair");
      button.classList.remove("addLocation__selected");

      const location = e.latlng;

      if (element === originContainer) {
        transitRequest.origin = location;
      }

      if (element === destinationContainer) {
        transitRequest.destination = location;
      }

      setLatLngToContainer(element, location);
      calculateTransit();
    });
  });
}

const includedModes = [];
const excludedModes = [];
const modes = {
  train: [
    "highSpeedTrain",
    "intercityTrain",
    "interRegionalTrain",
    "regionalTrain",
    "cityTrain",
    "subway",
    "lightRail",
    "monorail",
    "inclined",
  ],
  bus: ["bus", "privateBus", "busRapid"],
  ferry: ["ferry"],
  aerial: ["aerial", "flight", "spaceship"],
};

function updateTransitOptions() {
  const checkboxes = document.querySelectorAll('.modesChk[type="checkbox"]');
  const includeRadio = document.querySelector("#includeModes");
  const excludeRadio = document.querySelector("#excludeModes");

  function updateMode() {
    includedModes.length = 0;
    excludedModes.length = 0;
    Object.keys(modes).forEach((mode) => {
      const checkbox = document.querySelector(
        `#mode${mode.charAt(0).toUpperCase() + mode.slice(1)}`,
      );

      if (checkbox.checked) {
        modes[mode].forEach((subMode) => {
          if (includeRadio.checked) {
            includedModes.push(subMode);
          } else if (excludeRadio.checked) {
            excludedModes.push("-" + subMode);
          }
        });
      }
    });
    if (includedModes.length === 0 && excludedModes.length === 0) {
      delete transitRequest.modes;
    } else {
      transitRequest.modes = includeRadio.checked
        ? includedModes
        : excludedModes;
    }

    calculateTransit();
  }

  checkboxes.forEach((checkbox) => {
    checkbox.addEventListener("change", updateMode);
  });
  includeRadio.addEventListener("change", updateMode);
  excludeRadio.addEventListener("change", updateMode);
}

function initUI() {
  updateTime("departureTime");
  updateTime("arrivalTime");
  updateTransitOptions();
  registerAddButton(
    ".addLocation__destinations",
    destinationContainer,
    transitRequest.destination,
  );
  registerAddButton(
    ".addLocation__origins",
    originContainer,
    transitRequest.origin,
  );
}

function initMap() {
  map = new woosmap.map.Map(document.getElementById("map"), {
    center: { lat: 51.5074, lng: -0.1478 },
    zoom: 10,
  });
  transitService = new woosmap.map.TransitService();
  transitRenderer = new woosmap.map.TransitRenderer({});
  transitRenderer.setMap(map);
  originContainer = document.getElementById("origin");
  destinationContainer = document.getElementById("destination");
  transitRequest = createDefaultRequest();
  initUI();
  calculateTransit();
}

window.initMap = initMap;

    
        html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}

#container {
  height: 100%;
  display: flex;
}

#sidebar {
  flex-basis: 12rem;
  flex-grow: 1;
  max-width: 30rem;
  height: 100%;
  box-sizing: border-box;
  overflow: auto;
}

#map {
  flex-basis: 70vw;
  flex-grow: 5;
  height: 100%;
}

#sidebar {
  flex-basis: 18rem;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.12);
  z-index: 1;
}

#mapContainer {
  display: flex;
  flex-direction: column;
  flex-basis: 70vw;
  flex-grow: 5;
  position: relative;
}

.tableContainer {
  max-height: 45%;
  overflow-y: auto;
  font-size: 13px;
}

.directionTrip {
  cursor: pointer;
  color: rgba(0, 0, 0, 0.5411764706);
  border-bottom: 1px solid #e6e6e6;
  flex: none;
  padding: 1rem;
  position: relative;
}
.directionTrip__selected :before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  border-left: 5px solid #3d5afe;
  height: 100%;
}
.directionTrip__selected .directionTrip__detailsMsg {
  display: block;
}
.directionTrip__detailsMsg {
  display: none;
}
.directionTrip__travelModeIcon {
  position: absolute;
  top: 18px;
  width: 24px;
  height: 24px;
  opacity: 0.6;
}
.directionTrip__description {
  padding-left: 48px;
}
.directionTrip__numbers {
  float: right;
  text-align: right;
  padding-left: 10px;
}
.directionTrip__duration, .directionTrip__title {
  color: rgba(0, 0, 0, 0.87);
  font-size: 15px;
  vertical-align: top;
}
.directionTrip__duration {
  color: #188038;
}
.directionTrip__distance, .directionTrip__summary {
  line-height: 16px;
  padding: 4px 0;
  font-size: 13px;
}
.directionTrip__detailsMsg {
  font-size: 13px;
  font-weight: 500;
  text-transform: uppercase;
  color: #3d5afe;
}
.directionTrip__empty {
  padding: 1rem;
}
.directionTrip__error {
  margin-top: 1rem;
  font-size: 0.9rem;
}
.directionTrip__error:before {
  content: "";
  display: inline-block;
  background-size: cover;
  width: 17px;
  height: 15px;
  padding-right: 5px;
  overflow: hidden;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='48' width='48'%3E%3Cpath d='M2 42 24 4l22 38Zm22.2-5.85q.65 0 1.075-.425.425-.425.425-1.075 0-.65-.425-1.075-.425-.425-1.075-.425-.65 0-1.075.425Q22.7 34 22.7 34.65q0 .65.425 1.075.425.425 1.075.425Zm-1.5-5.55h3V19.4h-3Z' fill= '%23F3d5afe' /%3E%3C/svg%3E");
}
.directionTrip__summaryoverview {
  justify-content: start;
  align-items: center;
  padding-top: 10px;
  padding-left: 45px;
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  line-height: 2.25rem;
}
.directionTrip .step {
  display: contents;
}
.directionTrip .icon {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background-color: #ccc;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 0px;
  font-size: 20px;
  color: "#FFF";
}
.directionTrip .icon.bus {
  background-color: "#28a745";
  background-image: url('data:image/svg+xml,<svg width="50px" height="50px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><path d="M5 6V15.8C5 16.9201 5 17.4802 5.21799 17.908C5.40973 18.2843 5.71569 18.5903 6.09202 18.782C6.51984 19 7.07989 19 8.2 19H15.8C16.9201 19 17.4802 19 17.908 18.782C18.2843 18.5903 18.5903 18.2843 18.782 17.908C19 17.4802 19 16.9201 19 15.8V6M5 6C5 6 5 3 12 3C19 3 19 6 19 6M5 6H19M5 13H19M17 21V19M7 21V19M8 16H8.01M16 16H16.01" stroke="%23FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.directionTrip .icon.transit {
  background-color: #28a745;
}
.directionTrip .icon.metro {
  background-image: url('data:image/svg+xml,<svg fill="%23000000" width="20px" height="20px" viewBox="0 -8 72 72" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><title>metro-front</title><path d="M48.89,48.14h1.27a6,6,0,0,0,6-6v-36a6,6,0,0,0-6-6H21.84a6,6,0,0,0-6,6v36a6,6,0,0,0,6,6h1.27L14.55,55.8H20l2.74-2.45H49.31l2.74,2.45h5.4ZM48,44.45a3,3,0,1,1,3-3A2.95,2.95,0,0,1,48,44.45Zm-18.72-42H42.71a1.65,1.65,0,0,1,0,3.3H29.29a1.65,1.65,0,1,1,0-3.3ZM18.84,26.25V11.15a3,3,0,0,1,3-3H50.16a3,3,0,0,1,3,3v15.1a3,3,0,0,1-3,3H21.84A3,3,0,0,1,18.84,26.25ZM21,41.5a3,3,0,1,1,2.95,3A2.95,2.95,0,0,1,21,41.5Zm5.67,8.25,1.78-1.61H43.58l1.71,1.61Z"/></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.directionTrip .icon.pedestrian {
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
  background-image: url("https://images.woosmap.com/directions/walk_black.png");
}
.directionTrip .icon.tram {
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
  background-image: url('data:image/svg+xml,<svg width="100px" height="100px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><path d="M7 13H17M10 16H10.01M14 16H14.01M5 21L7.5 18.5M19 21L16.5 18.5M12 7V3M4 4L4.12132 3.87868C4.68393 3.31607 5.44699 3 6.24264 3H17.7574C18.553 3 19.3161 3.31607 19.8787 3.87868L20 4M10.2 19H13.8C14.9201 19 15.4802 19 15.908 18.782C16.2843 18.5903 16.5903 18.2843 16.782 17.908C17 17.4802 17 16.9201 17 15.8V10.2C17 9.0799 17 8.51984 16.782 8.09202C16.5903 7.71569 16.2843 7.40973 15.908 7.21799C15.4802 7 14.9201 7 13.8 7H10.2C9.07989 7 8.51984 7 8.09202 7.21799C7.71569 7.40973 7.40973 7.71569 7.21799 8.09202C7 8.51984 7 9.07989 7 10.2V15.8C7 16.9201 7 17.4802 7.21799 17.908C7.40973 18.2843 7.71569 18.5903 8.09202 18.782C8.51984 19 9.07989 19 10.2 19Z" stroke="%23FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>');
}
.directionTrip .icon.rail {
  background-image: url('data:image/svg+xml,<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" xml:space="preserve" width="100px" height="100px" fill="%23FFFFFF" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><style type="text/css"> .st0{fill:none;stroke:%23FFFFFF;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;} .st1{fill:none;stroke:%23FFFFFF;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:10;} </style><path class="st0" d="M21,25H11c-2.2,0-4-1.8-4-4V6c0-2.2,1.8-4,4-4h10c2.2,0,4,1.8,4,4v15C25,23.2,23.2,25,21,25z"/><rect x="7" y="7" class="st0" width="18" height="11"/><line class="st0" x1="11" y1="21" x2="13" y2="22"/><line class="st0" x1="19" y1="22" x2="21" y2="21"/><line class="st0" x1="7" y1="30" x2="12" y2="25"/><line class="st0" x1="25" y1="30" x2="20" y2="25"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.directionTrip .icon.ferry {
  background-image: url('data:image/svg+xml,<svg fill="%23FFFFFF" width="101px" height="101px" viewBox="-3.6 -3.6 43.20 43.20" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><title>ferry-line</title><path d="M29,25.1c-1.2,0-2.3,0.4-3.3,1.1c0,0,0,0,0,0c-1.1,1.1-3,1.1-4.1,0c-0.9-0.8-2.1-1.2-3.3-1.2c-1.2,0-2.3,0.4-3.2,1.2 c-1.2,1.1-3,1.1-4.2,0C10,25.4,8.8,25,7.6,25c-1.2,0-2.4,0.4-3.4,1.1C3.6,26.7,2.8,27,2,27v2c1.3,0.1,2.5-0.4,3.4-1.2 C6.1,27.3,6.9,27,7.7,27c0.8,0,1.5,0.3,2.1,0.8c1.9,1.6,4.7,1.6,6.5,0c0.6-0.5,1.3-0.8,2.1-0.8c0.8,0,1.5,0.3,2.1,0.8 c1.9,1.6,4.6,1.6,6.5,0c0.5-0.5,1.3-0.8,2-0.8c0.7,0,1.5,0.3,2,0.8c0.9,0.7,2,1.1,3.1,1.2v-1.9c-0.7,0-1.4-0.3-1.9-0.9 C31.3,25.4,30.1,25,29,25.1z" class="clr-i-outline clr-i-outline-path-1"/><path d="M5.9,23.2V20H32l-3.5,3h0.2c0.8,0,1.6,0.2,2.2,0.5l2.5-2.2l0.2-0.2c0.5-0.6,0.5-1.4,0.2-2.1c-0.4-0.7-1-1-1.8-1h-4.4 L22.5,11H17c-1.7,0-3,1.3-3,3h-2V8.1H6v6.1c-1.2,0.4-2,1.5-2,2.8v1.1V20v4.3l0.1-0.1C4.6,23.7,5.2,23.4,5.9,23.2z M8,10h2v4H8V10z M6,17c0-0.6,0.4-1,1-1h9v-2c0-0.6,0.4-1,1-1h5l0.6,1H18v2h5.8l1.2,2.1H6V17z" class="clr-i-outline clr-i-outline-path-2"/><rect x="0" y="0" width="36" height="36" fill-opacity="0"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.directionTrip .pill {
  color: #fff;
  background-color: #e0e0e0;
  border-radius: 15px;
  align-items: center;
  padding: 4px 10px;
  font-size: 12px;
  display: flex;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1019607843);
  gap: 4px;
  height: 20px;
}
.directionTrip .transitduration {
  font-size: 12px;
  color: #fff;
  font-weight: bold;
}
.directionTrip .step-container {
  display: flex;
  gap: 4px;
  align-items: center;
}

#innerWrapper {
  display: flex;
  flex-direction: column;
  flex-grow: 1;
  overflow: hidden;
  overflow-y: auto;
  padding: 0 10px 40px;
}

input,
select {
  font-family: inherit;
  font-size: 100%;
  box-sizing: border-box;
}

.transitOptions {
  padding: 0;
  margin: 0;
  list-style: none;
  height: 100%;
  background: #fff;
  display: flex;
  font-size: 13px;
}
.transitOptions__list {
  width: 100%;
  height: 100%;
}
.transitOptions__header {
  font-weight: 600;
  line-height: 24px;
  display: flex;
}
.transitOptions__input {
  height: 24px;
  display: flex;
  align-items: baseline;
}
.transitOptions__content {
  padding: 10px 0;
}
.transitOptions__content:first-child {
  padding-top: 0;
}

.addLocation {
  font-size: 0.75em;
  display: flex;
  color: #222;
  cursor: pointer;
}
.addLocation:hover {
  text-decoration: underline;
}
.addLocation div {
  margin-left: 5px;
}
.addLocation__selected {
  color: #3d5afe;
}
.addLocation__selected svg path {
  fill: #3d5afe;
}

.sectionHeader {
  background: #f1f1f1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid #eeeeee;
  margin: 20px -10px 5px;
  padding: 5px 10px;
  color: #222;
}
.sectionHeader span {
  font-size: 0.85em;
  font-weight: 600;
}
.sectionHeader:first-child {
  margin-top: 0;
}

.customCounter {
  margin: 0;
  padding: 0;
  list-style-type: none;
  width: 100%;
}

.customCounter li {
  counter-increment: step-counter;
  line-height: 14px;
  height: 22px;
  display: flex;
  align-items: center;
}
.customCounter li span {
  flex-grow: 1;
}

.customCounter li::before {
  margin-right: 5px;
  font-size: 80%;
  color: #fff;
  background-position: bottom;
  font-weight: 600;
  width: 20px;
  height: 20px;
  text-align: center;
  background-size: contain;
  background-repeat: no-repeat;
}

.customCounter__destination li::before {
  background-image: url(https://images.woosmap.com/marker-red.svg);
  content: "D";
}

.customCounter__origin li::before {
  background-image: url(https://images.woosmap.com/marker-blue.svg);
  content: "O";
}

.clear-searchButton {
  display: block;
  height: 18px;
  width: 22px;
  background: none;
  border: none;
  pointer-events: all;
  cursor: pointer;
}
.clear-searchButton .clear-icon {
  color: inherit;
  flex-shrink: 0;
  height: 16px;
  width: 16px;
}

#map.cursor-crosshair .mapboxgl-canvas-container {
  cursor: crosshair !important;
}

#infoMessage {
  font-size: 12px;
  max-width: 200px;
  position: absolute;
  top: 0;
  background-color: #fff;
  border-radius: 3px;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
  margin: 10px;
  padding: 5px;
  overflow: hidden;
  z-index: 1;
}

.linear-progress {
  position: absolute;
  width: 100%;
  z-index: 2;
  bottom: 0;
  height: 4px;
  border-radius: 3px;
  overflow: hidden;
}

.progress {
  height: 100%;
  animation: progress 1.5s infinite;
  background: linear-gradient(to right, #3D5AFE, #3D5AFE);
  transform: translateX(-100%);
}

@keyframes progress {
  0% {
    transform: translateX(-100%);
  }
  50% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(100%);
  }
}
.linear-progress.hide {
  display: none;
}

.trouteRoadbook {
  flex-direction: column;
  flex: 1 0 0;
  display: flex;
  overflow: hidden;
}
.trouteRoadbook__steps {
  padding: 1rem 0 0;
}
.trouteRoadbook__body {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
}
.trouteRoadbook__body.active:before {
  z-index: 2;
  content: "";
  width: 100%;
  height: 14px;
  position: absolute;
  box-shadow: inset 0 14px 10px -10px rgba(60, 64, 67, 0.28);
}
.trouteRoadbook__body .transit-summary {
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 20px;
}
.trouteRoadbook__body .route {
  display: flex;
  align-items: top;
  margin-bottom: 15px;
}
.trouteRoadbook__body .route:last-child {
  margin-bottom: 0;
}
.trouteRoadbook__body .icon {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background-color: #ccc;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 15px;
  font-size: 20px;
  color: "#fff";
}
.trouteRoadbook__body .icon.bus {
  background-color: "#28a745";
  background-image: url('data:image/svg+xml,<svg width="50px" height="50px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><path d="M5 6V15.8C5 16.9201 5 17.4802 5.21799 17.908C5.40973 18.2843 5.71569 18.5903 6.09202 18.782C6.51984 19 7.07989 19 8.2 19H15.8C16.9201 19 17.4802 19 17.908 18.782C18.2843 18.5903 18.5903 18.2843 18.782 17.908C19 17.4802 19 16.9201 19 15.8V6M5 6C5 6 5 3 12 3C19 3 19 6 19 6M5 6H19M5 13H19M17 21V19M7 21V19M8 16H8.01M16 16H16.01" stroke="%23FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.trouteRoadbook__body .icon.transit {
  background-color: #28a745;
}
.trouteRoadbook__body .icon.metro {
  background-image: url('data:image/svg+xml,<svg fill="%23000000" width="20px" height="20px" viewBox="0 -8 72 72" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><title>metro-front</title><path d="M48.89,48.14h1.27a6,6,0,0,0,6-6v-36a6,6,0,0,0-6-6H21.84a6,6,0,0,0-6,6v36a6,6,0,0,0,6,6h1.27L14.55,55.8H20l2.74-2.45H49.31l2.74,2.45h5.4ZM48,44.45a3,3,0,1,1,3-3A2.95,2.95,0,0,1,48,44.45Zm-18.72-42H42.71a1.65,1.65,0,0,1,0,3.3H29.29a1.65,1.65,0,1,1,0-3.3ZM18.84,26.25V11.15a3,3,0,0,1,3-3H50.16a3,3,0,0,1,3,3v15.1a3,3,0,0,1-3,3H21.84A3,3,0,0,1,18.84,26.25ZM21,41.5a3,3,0,1,1,2.95,3A2.95,2.95,0,0,1,21,41.5Zm5.67,8.25,1.78-1.61H43.58l1.71,1.61Z"/></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.trouteRoadbook__body .icon.pedestrian {
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
  background-image: url('data:image/svg+xml, <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="48.000000pt" height="48.000000pt" viewBox="0 0 48.000000 48.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,48.000000) scale(0.100000,-0.100000)" fill="%23ffffff" stroke="none"><path d="M250 445 c-15 -18 -10 -45 13 -59 34 -22 73 27 47 59 -16 19 -44 19 -60 0z"/><path d="M183 347 l-53 -22 0 -47 c0 -41 3 -48 19 -48 16 0 20 8 23 37 2 25 8 39 19 41 14 3 13 -14 -12 -130 -16 -73 -29 -137 -29 -141 0 -5 10 -7 22 -5 20 3 25 13 37 78 7 41 15 77 17 78 1 2 12 -6 23 -17 17 -17 21 -34 21 -81 0 -53 2 -60 20 -60 18 0 20 7 20 80 0 69 -3 82 -20 92 -20 13 -25 34 -14 61 5 13 11 11 41 -9 43 -29 73 -31 73 -6 0 11 -12 22 -34 30 -20 8 -44 28 -60 52 -30 45 -40 47 -113 17z"/></g></svg>');
}
.trouteRoadbook__body .icon.tram {
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
  background-image: url('data:image/svg+xml,<svg width="100px" height="100px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><path d="M7 13H17M10 16H10.01M14 16H14.01M5 21L7.5 18.5M19 21L16.5 18.5M12 7V3M4 4L4.12132 3.87868C4.68393 3.31607 5.44699 3 6.24264 3H17.7574C18.553 3 19.3161 3.31607 19.8787 3.87868L20 4M10.2 19H13.8C14.9201 19 15.4802 19 15.908 18.782C16.2843 18.5903 16.5903 18.2843 16.782 17.908C17 17.4802 17 16.9201 17 15.8V10.2C17 9.0799 17 8.51984 16.782 8.09202C16.5903 7.71569 16.2843 7.40973 15.908 7.21799C15.4802 7 14.9201 7 13.8 7H10.2C9.07989 7 8.51984 7 8.09202 7.21799C7.71569 7.40973 7.40973 7.71569 7.21799 8.09202C7 8.51984 7 9.07989 7 10.2V15.8C7 16.9201 7 17.4802 7.21799 17.908C7.40973 18.2843 7.71569 18.5903 8.09202 18.782C8.51984 19 9.07989 19 10.2 19Z" stroke="%23FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>');
}
.trouteRoadbook__body .icon.rail {
  background-image: url('data:image/svg+xml,<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" xml:space="preserve" width="100px" height="100px" fill="%23FFFFFF" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><style type="text/css"> .st0{fill:none;stroke:%23FFFFFF;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;} .st1{fill:none;stroke:%23FFFFFF;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:10;} </style><path class="st0" d="M21,25H11c-2.2,0-4-1.8-4-4V6c0-2.2,1.8-4,4-4h10c2.2,0,4,1.8,4,4v15C25,23.2,23.2,25,21,25z"/><rect x="7" y="7" class="st0" width="18" height="11"/><line class="st0" x1="11" y1="21" x2="13" y2="22"/><line class="st0" x1="19" y1="22" x2="21" y2="21"/><line class="st0" x1="7" y1="30" x2="12" y2="25"/><line class="st0" x1="25" y1="30" x2="20" y2="25"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.trouteRoadbook__body .icon.ferry {
  background-image: url('data:image/svg+xml,<svg fill="%23FFFFFF" width="101px" height="101px" viewBox="-3.6 -3.6 43.20 43.20" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" stroke="%23FFFFFF"><g id="SVGRepo_bgCarrier" stroke-width="0"/><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/><g id="SVGRepo_iconCarrier"><title>ferry-line</title><path d="M29,25.1c-1.2,0-2.3,0.4-3.3,1.1c0,0,0,0,0,0c-1.1,1.1-3,1.1-4.1,0c-0.9-0.8-2.1-1.2-3.3-1.2c-1.2,0-2.3,0.4-3.2,1.2 c-1.2,1.1-3,1.1-4.2,0C10,25.4,8.8,25,7.6,25c-1.2,0-2.4,0.4-3.4,1.1C3.6,26.7,2.8,27,2,27v2c1.3,0.1,2.5-0.4,3.4-1.2 C6.1,27.3,6.9,27,7.7,27c0.8,0,1.5,0.3,2.1,0.8c1.9,1.6,4.7,1.6,6.5,0c0.6-0.5,1.3-0.8,2.1-0.8c0.8,0,1.5,0.3,2.1,0.8 c1.9,1.6,4.6,1.6,6.5,0c0.5-0.5,1.3-0.8,2-0.8c0.7,0,1.5,0.3,2,0.8c0.9,0.7,2,1.1,3.1,1.2v-1.9c-0.7,0-1.4-0.3-1.9-0.9 C31.3,25.4,30.1,25,29,25.1z" class="clr-i-outline clr-i-outline-path-1"/><path d="M5.9,23.2V20H32l-3.5,3h0.2c0.8,0,1.6,0.2,2.2,0.5l2.5-2.2l0.2-0.2c0.5-0.6,0.5-1.4,0.2-2.1c-0.4-0.7-1-1-1.8-1h-4.4 L22.5,11H17c-1.7,0-3,1.3-3,3h-2V8.1H6v6.1c-1.2,0.4-2,1.5-2,2.8v1.1V20v4.3l0.1-0.1C4.6,23.7,5.2,23.4,5.9,23.2z M8,10h2v4H8V10z M6,17c0-0.6,0.4-1,1-1h9v-2c0-0.6,0.4-1,1-1h5l0.6,1H18v2h5.8l1.2,2.1H6V17z" class="clr-i-outline clr-i-outline-path-2"/><rect x="0" y="0" width="36" height="36" fill-opacity="0"/></g></svg>');
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  justify-content: center;
}
.trouteRoadbook__body .details {
  flex: 1;
  letter-spacing: 0;
  font-size: 0.875rem;
  font-weight: 400;
  line-height: 1.25rem;
}
.trouteRoadbook__body .subdetails {
  border-left: 2px dashed gray;
  margin-left: -30px;
  padding-left: 40px;
}
.trouteRoadbook__body .line {
  font-weight: bold;
}
.trouteRoadbook__body .time,
.trouteRoadbook__body .duration {
  display: block;
  color: #555;
  font-size: 0.75rem;
  line-height: 1rem;
}
.trouteRoadbook__body .divider {
  padding-top: 10px;
  border-bottom: 0.08rem solid rgb(218, 220, 224);
}
.trouteRoadbook__step {
  position: relative;
  padding: 1rem 0 1rem 2rem;
  cursor: pointer;
  text-align: left;
  width: 100%;
  box-sizing: border-box;
  display: inline-block;
}
.trouteRoadbook__stepBody {
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.25rem;
}
.trouteRoadbook__stepDistance {
  position: relative;
  font-size: 0.75rem;
  letter-spacing: 0;
  line-height: 1rem;
  color: rgba(0, 0, 0, 0.54);
  padding-left: 2rem;
}
.trouteRoadbook__stepDistanceSeparator {
  border-bottom: 1px solid #e6e6e6;
}
.trouteRoadbook__stepDistanceText {
  display: inline-block;
  position: absolute;
  top: -7px;
  background-color: #fff;
  padding-right: 5px;
  z-index: 1;
}
.trouteRoadbook__stepIcon {
  overflow: hidden;
  position: absolute;
  top: 0;
  left: 0;
  width: 18px;
  height: 18px;
  margin-top: 1rem;
}
.trouteRoadbook__stepIcon img {
  height: 18px;
  aspect-ratio: auto 18/18;
  width: 18px;
}
.trouteRoadbook__stepIconreverse {
  transform: scaleX(-1);
}


    
        <html>
  <head>
    <title>Transit Service With Roadbook</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta charset="utf-8" />

    <link rel="stylesheet" type="text/css" href="./style.css" />
    <script type="module" src="./index.js"></script>
  </head>
  <body>
    <div id="container">
      <div id="sidebar">
        <div id="innerWrapper">
          <div class="sectionHeader">
            <span>Origin</span>
            <div
              aria-label="Set Origin"
              class="addLocation addLocation__origins"
            >
              <svg
                width="10"
                height="14"
                viewBox="0 0 10 14"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M4.21278471,13.2173279 C4.21344203,13.4210003 4.29852035,13.6152829 4.447752,13.7538931 C4.59698368,13.8925033 4.79700821,13.9630315 5.00017514,13.9486763 C5.20260653,13.9603467 5.40103075,13.8888849 5.54953735,13.7508253 C5.69804403,13.6127657 5.78376442,13.4200717 5.786865,13.2173279 C5.786865,11.8485015 6.73642703,10.7280113 7.74168082,9.54202199 C8.85201342,8.23203971 9.9998241,6.87722382 9.9998241,5.04184757 C10.0148585,3.24561164 9.06519921,1.57935055 7.51207057,0.676864524 C5.958942,-0.225621508 4.04105803,-0.225621508 2.48792942,0.676864524 C0.93480081,1.57935055 -0.0148585204,3.24561164 0.000175901018,5.04184757 C0.000175901018,6.87722382 1.14833685,8.23203971 2.25901969,9.54202199 C3.26322269,10.7280113 4.21278471,11.8485015 4.21278471,13.2173279 Z"
                  fill="#787878"
                />
              </svg>
              <div>Set origin</div>
            </div>
          </div>
          <div class="transitOptions">
            <ol id="origin" class="customCounter customCounter__origin"></ol>
          </div>
          <div class="sectionHeader">
            <span>Destination</span>
            <div
              aria-label="Set Destination"
              class="addLocation addLocation__destinations"
            >
              <svg
                width="10"
                height="14"
                viewBox="0 0 10 14"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M4.21278471,13.2173279 C4.21344203,13.4210003 4.29852035,13.6152829 4.447752,13.7538931 C4.59698368,13.8925033 4.79700821,13.9630315 5.00017514,13.9486763 C5.20260653,13.9603467 5.40103075,13.8888849 5.54953735,13.7508253 C5.69804403,13.6127657 5.78376442,13.4200717 5.786865,13.2173279 C5.786865,11.8485015 6.73642703,10.7280113 7.74168082,9.54202199 C8.85201342,8.23203971 9.9998241,6.87722382 9.9998241,5.04184757 C10.0148585,3.24561164 9.06519921,1.57935055 7.51207057,0.676864524 C5.958942,-0.225621508 4.04105803,-0.225621508 2.48792942,0.676864524 C0.93480081,1.57935055 -0.0148585204,3.24561164 0.000175901018,5.04184757 C0.000175901018,6.87722382 1.14833685,8.23203971 2.25901969,9.54202199 C3.26322269,10.7280113 4.21278471,11.8485015 4.21278471,13.2173279 Z"
                  fill="#787878"
                />
              </svg>
              <div>Set destination</div>
            </div>
          </div>
          <div class="transitOptions">
            <ol
              id="destination"
              class="customCounter customCounter__destination"
            ></ol>
          </div>
          <div class="sectionHeader"><span>Options</span></div>
          <div class="transitOptions">
            <div class="transitOptions__list">
              <div class="transitOptions__content">
                <span class="transitOptions__header">Departure time</span>
                <div class="transitOptions__input">
                  <input
                    id="empty"
                    aria-label="Empty"
                    name="departureTime"
                    type="radio"
                    value="empty"
                    checked=""
                  />
                  <label for="empty">Not Define (now)</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    id="datetime"
                    aria-label="Datetime"
                    name="departureTime"
                    type="radio"
                    value="datetime"
                  />
                  <input
                    id="departureTime-time"
                    name="departure-time"
                    type="datetime-local"
                  />
                </div>
              </div>
            </div>
          </div>
          <div class="transitOptions">
            <div class="transitOptions__list">
              <div class="transitOptions__content">
                <span class="transitOptions__header">Arrival time</span>
                <div class="transitOptions__input">
                  <input
                    id="empty"
                    aria-label="Empty"
                    name="arrivalTime"
                    type="radio"
                    value="empty"
                    checked=""
                  />
                  <label for="empty">Not Define</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    id="datetime"
                    aria-label="Datetime"
                    name="arrivalTime"
                    type="radio"
                    value="datetime"
                  />
                  <input
                    id="arrivalTime-time"
                    name="arrival-time"
                    type="datetime-local"
                  />
                </div>
              </div>
            </div>
          </div>

          <div class="transitOptions">
            <div class="transitOptions__list">
              <div class="transitOptions__content">
                <span class="transitOptions__header">Specify modes</span>
                <div class="transitOptions__input">
                  <input
                    id="includeModes"
                    aria-label="include"
                    name="modeOption"
                    type="radio"
                    value="include"
                    checked=""
                  />
                  <label for="empty">Include only</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    id="excludeModes"
                    aria-label="exclude"
                    name="modeOption"
                    type="radio"
                    value="exclude"
                  />
                  <label for="empty">Exclude</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    class="modesChk"
                    id="modeTrain"
                    aria-label="Mode Train"
                    name="train"
                    type="checkbox"
                  />
                  <label for="modeTrain">Train</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    class="modesChk"
                    id="modeBus"
                    aria-label="Mode Bus"
                    name="bus"
                    type="checkbox"
                  />
                  <label for="modeBus">Bus</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    class="modesChk"
                    id="modeFerry"
                    aria-label="ferry"
                    name="modeFerry"
                    type="checkbox"
                  />
                  <label for="modeFerry">Ferry</label>
                </div>
                <div class="transitOptions__input">
                  <input
                    class="modesChk"
                    id="modeAerial"
                    aria-label="aerial"
                    name="modeAerial"
                    type="checkbox"
                  />
                  <label for="modeAerial">Aerial</label>
                </div>
              </div>
            </div>
          </div>
          <div class="sectionHeader"><span>Roadbook</span></div>
          <div class="transitOptions" id="roadbook__container"></div>
        </div>
      </div>
      <div id="mapContainer">
        <div class="linear-progress hide">
          <div class="progress"></div>
        </div>
        <div id="map"></div>
        <div id="infoMessage">Drag markers to update route</div>
        <div class="tableContainer"></div>
      </div>
    </div>


    <script
      src="https://sdk.woosmap.com/map/map.js?key=woos-48c80350-88aa-333e-835a-07f4b658a9a4&callback=initMap"
      defer
    ></script>
  </body>
</html>

    

Running the Sample Locally

Before you can run this sample on your local machine, you need to have Git and Node.js installed. If they’re not already installed, follow these instructions to get them set up.

Once you have Git and Node.js installed, you can run the sample by following these steps:

  1. Clone the repository and navigate to the directory of the sample.

  2. Install the necessary dependencies.

  3. Start running the sample.

Here are the commands you can use in your terminal to do this:

Shell
        git clone -b sample/transit-roadbook https://github.com/woosmap/js-samples.git
cd js-samples
npm i
npm start

    

You can experiment with other samples by switching to any branch that starts with the pattern sample/SAMPLE_NAME.

Shell
        git checkout sample/SAMPLE_NAME
npm i
npm start

    
Was this article helpful?
Have more questions? Submit a request