diff --git a/src/TripObjects.js b/src/TripObjects.js
new file mode 100644
index 0000000..b12040a
--- /dev/null
+++ b/src/TripObjects.js
@@ -0,0 +1,217 @@
+// src/TripObjects.js
+
+import { log } from "./Utils";
+import { getColor } from "./Trip";
+
+export class TripObjects {
+ constructor({ map, setFeaturedObject, setTimeRange }) {
+ this.map = map;
+ this.markers = new Map();
+ this.paths = new Map();
+ this.arrows = new Map();
+ this.setFeaturedObject = setFeaturedObject;
+ this.setTimeRange = setTimeRange;
+ }
+
+ createSVGMarker(type, color) {
+ const isPickup = type.toLowerCase().includes("pickup");
+ const isActual = type.includes("actual");
+
+ const createChevronPath = (direction, shift = 0) => {
+ let points;
+ if (direction === "up") {
+ points = [
+ { x: 12, y: 20 + shift }, // Top point of chevron
+ { x: 8, y: 24 + shift }, // Bottom left
+ { x: 16, y: 24 + shift }, // Bottom right
+ { x: 12, y: 20 + shift },
+ ];
+ } else {
+ // "down"
+ points = [
+ { x: 12, y: 24 + shift }, // Bottom point of chevron
+ { x: 8, y: 20 + shift }, // Top left
+ { x: 16, y: 20 + shift }, // Top right
+ { x: 12, y: 24 + shift },
+ ];
+ }
+
+ return ``;
+ };
+
+ const svgBase = `
+ `;
+
+ return {
+ url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svgBase),
+ scaledSize: new google.maps.Size(24, 24),
+ anchor: new google.maps.Point(12, 24),
+ };
+ }
+
+ createMarkerWithEvents(point, type, color, pairedPoint, tripId) {
+ if (!point) return null;
+
+ const marker = new google.maps.Marker({
+ position: { lat: point.latitude, lng: point.longitude },
+ icon: this.createSVGMarker(type, color),
+ map: this.map,
+ });
+
+ if (pairedPoint) {
+ // Store the arrow in the class instance using a unique key
+ const arrowKey = `${tripId}_${type.includes("actual") ? type.replace("actual", "") : type}`;
+
+ google.maps.event.addListener(marker, "click", () => {
+ log(`${type} marker clicked`);
+
+ if (this.arrows.has(arrowKey)) {
+ this.arrows.get(arrowKey).setMap(null);
+ this.arrows.delete(arrowKey);
+ } else {
+ // Create arrow from requested to actual
+ const from = type.includes("actual") ? pairedPoint : point;
+ const to = type.includes("actual") ? point : pairedPoint;
+ const arrow = this.createConnectingArrow(from, to, color);
+ if (arrow) {
+ this.arrows.set(arrowKey, arrow);
+ }
+ }
+ });
+ }
+
+ return marker;
+ }
+
+ createConnectingArrow(from, to, color) {
+ if (!from || !to) return null;
+
+ return new google.maps.Polyline({
+ path: [
+ { lat: from.latitude, lng: from.longitude },
+ { lat: to.latitude, lng: to.longitude },
+ ],
+ geodesic: true,
+ strokeColor: color,
+ strokeOpacity: 1,
+ strokeWeight: 1,
+ icons: [
+ {
+ icon: {
+ path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
+ scale: 1,
+ },
+ offset: "100%",
+ },
+ ],
+ map: this.map,
+ });
+ }
+
+ addTripVisuals(trip, minDate, maxDate) {
+ const tripId = trip.tripName;
+ log(`Processing trip visuals for ${tripId}`, {
+ pickupPoint: trip.getPickupPoint(),
+ actualPickupPoint: trip.getActualPickupPoint(),
+ dropoffPoint: trip.getDropoffPoint(),
+ actualDropoffPoint: trip.getActualDropoffPoint(),
+ });
+
+ this.clearTripObjects(tripId);
+
+ // Add path polyline
+ const tripCoords = trip.getPathCoords(minDate, maxDate);
+ if (tripCoords.length > 0) {
+ const path = new google.maps.Polyline({
+ path: tripCoords,
+ geodesic: true,
+ strokeColor: getColor(trip.tripIdx),
+ strokeOpacity: 0.5,
+ strokeWeight: 6,
+ map: this.map,
+ });
+
+ // Add path events
+ google.maps.event.addListener(path, "mouseover", () => {
+ path.setOptions({ strokeOpacity: 1, strokeWeight: 8 });
+ });
+
+ google.maps.event.addListener(path, "mouseout", () => {
+ path.setOptions({ strokeOpacity: 0.5, strokeWeight: 6 });
+ });
+
+ google.maps.event.addListener(path, "click", () => {
+ log("Trip polyline clicked");
+ const fd = trip.getFeaturedData();
+ this.setFeaturedObject(fd);
+ // TODO: https://github.com/googlemaps/fleet-debugger/issues/79
+ // this time range won't capture the createTrip logs
+ this.setTimeRange(fd.firstUpdate.getTime(), fd.lastUpdate.getTime());
+ });
+
+ this.paths.set(tripId, path);
+ }
+
+ const markers = [];
+
+ // Get points
+ const pickupPoint = trip.getPickupPoint();
+ const actualPickupPoint = trip.getActualPickupPoint();
+ const dropoffPoint = trip.getDropoffPoint();
+ const actualDropoffPoint = trip.getActualDropoffPoint();
+
+ // Create pickup markers
+ const pickupMarker = this.createMarkerWithEvents(pickupPoint, "pickup", "#3d633d", actualPickupPoint, tripId);
+ if (pickupMarker) markers.push(pickupMarker);
+
+ const actualPickupMarker = this.createMarkerWithEvents(
+ actualPickupPoint,
+ "actualPickup",
+ "#3d633d",
+ pickupPoint,
+ tripId
+ );
+ if (actualPickupMarker) markers.push(actualPickupMarker);
+
+ // Create dropoff markers
+ const dropoffMarker = this.createMarkerWithEvents(dropoffPoint, "dropoff", "#0000FF", actualDropoffPoint, tripId);
+ if (dropoffMarker) markers.push(dropoffMarker);
+
+ const actualDropoffMarker = this.createMarkerWithEvents(
+ actualDropoffPoint,
+ "actualDropoff",
+ "#0000FF",
+ dropoffPoint,
+ tripId
+ );
+ if (actualDropoffMarker) markers.push(actualDropoffMarker);
+
+ this.markers.set(tripId, markers);
+ }
+
+ clearTripObjects(tripId) {
+ if (this.paths.has(tripId)) {
+ this.paths.get(tripId).setMap(null);
+ this.paths.delete(tripId);
+ }
+
+ if (this.markers.has(tripId)) {
+ this.markers.get(tripId).forEach((marker) => marker.setMap(null));
+ this.markers.delete(tripId);
+ }
+
+ if (this.arrows.has(tripId)) {
+ this.arrows.get(tripId).forEach((arrow) => arrow.setMap(null));
+ this.arrows.delete(tripId);
+ }
+ }
+
+ clearAll() {
+ for (const tripId of this.paths.keys()) {
+ this.clearTripObjects(tripId);
+ }
+ }
+}