diff --git a/package.json b/package.json
index 9399d863..b6773aaa 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
- "internal": "PUBLIC_URL=/fleet-debugger npm run build && rsync -avzhe ssh --progress --delete --inplace ./build/ ${CLOUDTOP}:${CLOUDPATH}",
+ "internal": "cp datasets/two-trips-bay-area.json public/data.json && PUBLIC_URL=/fleet-debugger npm run build && rsync -avzhe ssh --progress --delete --inplace ./build/ ${CLOUDTOP}:${CLOUDPATH}",
"test": "react-scripts test",
"eject": "react-scripts eject",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
diff --git a/src/Map.js b/src/Map.js
index f2537504..a8148cfd 100644
--- a/src/Map.js
+++ b/src/Map.js
@@ -275,7 +275,13 @@ function MapComponent({
log("handlePolylineSubmit called.");
const path = waypoints.map((wp) => new window.google.maps.LatLng(wp.latitude, wp.longitude));
- const newPolyline = new window.google.maps.Polyline({ path, geodesic: true, ...properties });
+ const newPolyline = new window.google.maps.Polyline({
+ path,
+ geodesic: true,
+ strokeColor: properties.color,
+ strokeOpacity: properties.opacity,
+ strokeWeight: properties.strokeWeight,
+ });
newPolyline.setMap(map);
setPolylines((prev) => [...prev, newPolyline]);
}, []);
diff --git a/src/PolylineCreation.js b/src/PolylineCreation.js
index da5273f3..487152ed 100644
--- a/src/PolylineCreation.js
+++ b/src/PolylineCreation.js
@@ -1,8 +1,8 @@
// src/PolylineCreation.js
import { useState } from "react";
-import { decode } from "s2polyline-ts";
import { log } from "./Utils";
+import { parsePolylineInput } from "./PolylineUtils";
function PolylineCreation({ onSubmit, onClose, buttonPosition }) {
const [input, setInput] = useState("");
@@ -13,60 +13,20 @@ function PolylineCreation({ onSubmit, onClose, buttonPosition }) {
const handleSubmit = (e) => {
e.preventDefault();
try {
- const trimmedInput = input.trim();
-
- // Check if input looks like an encoded polyline (single string without spaces)
- if (/^[A-Za-z0-9+/=\-_]+$/.test(trimmedInput)) {
- log("Attempting to decode S2 polyline:", trimmedInput);
- const decodedPoints = decode(trimmedInput);
-
- if (decodedPoints && decodedPoints.length > 0) {
- // Convert S2 points to our expected format
- const validWaypoints = decodedPoints.map((point) => ({
- latitude: point.latDegrees(),
- longitude: point.lngDegrees(),
- }));
-
- log(`Decoded ${validWaypoints.length} points from S2 polyline`);
- onSubmit(validWaypoints, { opacity, color, strokeWeight });
- setInput("");
- return;
- }
- }
-
- // Existing JSON parsing logic
- const jsonString = trimmedInput.replace(/(\w+):/g, '"$1":').replace(/\s+/g, " ");
-
- const inputWithBrackets = jsonString.startsWith("[") && jsonString.endsWith("]") ? jsonString : `[${jsonString}]`;
-
- const waypoints = JSON.parse(inputWithBrackets);
-
- const validWaypoints = waypoints.filter(
- (waypoint) =>
- typeof waypoint === "object" &&
- "latitude" in waypoint &&
- "longitude" in waypoint &&
- typeof waypoint.latitude === "number" &&
- typeof waypoint.longitude === "number"
- );
-
- if (validWaypoints.length === 0) {
- throw new Error("No valid waypoints found");
- }
-
+ const validWaypoints = parsePolylineInput(input);
log(`Parsed ${validWaypoints.length} valid waypoints`);
onSubmit(validWaypoints, { opacity, color, strokeWeight });
+ setInput("");
} catch (error) {
log("Invalid input format:", error);
}
- setInput("");
};
let placeholder = `Paste waypoints here:
{ latitude: 52.5163, longitude: 13.2399 },
{ latitude: 52.5162, longitude: 13.2400 }
-Or paste an encoded S2 polyline string`;
+Or paste an encoded S2 or Google Maps polyline string`;
return (
}
+ */
+export function decodeGooglePolyline(encoded) {
+ const points = [];
+ let index = 0,
+ len = encoded.length;
+ let lat = 0,
+ lng = 0;
+
+ while (index < len) {
+ let b,
+ shift = 0,
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlat = result & 1 ? ~(result >> 1) : result >> 1;
+ lat += dlat;
+
+ shift = 0;
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlng = result & 1 ? ~(result >> 1) : result >> 1;
+ lng += dlng;
+
+ points.push({
+ latitude: lat / 1e5,
+ longitude: lng / 1e5,
+ });
+ }
+
+ return points;
+}
+
+/**
+ * Parses polyline input in various formats: S2, Google Encoded, JSON, or Plain Text.
+ * @param {string} input
+ * @returns {Array<{latitude: number, longitude: number}>}
+ */
+export function parsePolylineInput(input) {
+ const trimmedInput = input.trim();
+
+ // Check if it's obviously JSON or plain text coordinate list
+ const isJsonLike = (trimmedInput.startsWith("[") || trimmedInput.startsWith("{")) && trimmedInput.includes(":");
+
+ if (!isJsonLike) {
+ try {
+ // S2 strings usually don't have spaces or certain JSON characters
+ if (!trimmedInput.includes(" ") && !trimmedInput.includes('"')) {
+ const decodedPoints = decodeS2(trimmedInput);
+ if (decodedPoints && decodedPoints.length > 0) {
+ return decodedPoints.map((point) => ({
+ latitude: point.latDegrees(),
+ longitude: point.lngDegrees(),
+ }));
+ }
+ }
+ } catch (e) {
+ // Continue to next format
+ }
+
+ try {
+ // Sanity check: Google polylines shouldn't have spaces or newlines
+ if (!trimmedInput.includes("\n") && !trimmedInput.includes(" ")) {
+ const decodedPoints = decodeGooglePolyline(trimmedInput);
+ if (decodedPoints && decodedPoints.length > 0) {
+ return decodedPoints;
+ }
+ }
+ } catch (e) {
+ // Continue to next format
+ }
+ }
+
+ try {
+ const jsonString = trimmedInput.replace(/(\w+):/g, '"$1":').replace(/\s+/g, " ");
+ const inputWithBrackets = jsonString.startsWith("[") && jsonString.endsWith("]") ? jsonString : `[${jsonString}]`;
+ const waypoints = JSON.parse(inputWithBrackets);
+
+ const validWaypoints = waypoints.filter(
+ (waypoint) =>
+ typeof waypoint === "object" &&
+ "latitude" in waypoint &&
+ "longitude" in waypoint &&
+ typeof waypoint.latitude === "number" &&
+ typeof waypoint.longitude === "number"
+ );
+
+ if (validWaypoints.length > 0) {
+ return validWaypoints;
+ }
+ } catch (e) {
+ // Fall through to error
+ }
+
+ throw new Error("Invalid polyline format or no valid coordinates found.");
+}
diff --git a/src/PolylineUtils.test.js b/src/PolylineUtils.test.js
new file mode 100644
index 00000000..fa2f214d
--- /dev/null
+++ b/src/PolylineUtils.test.js
@@ -0,0 +1,59 @@
+import { parsePolylineInput } from "./PolylineUtils";
+
+describe("PolylineUtils", () => {
+ const EXPECTED_POINTS = [
+ { latitude: 37.42213, longitude: -122.0848 },
+ { latitude: 37.4152, longitude: -122.0627 },
+ { latitude: 37.427, longitude: -122.0854 },
+ ];
+
+ test("decodes a GMP Encoded Polyline", () => {
+ const encoded = "i_lcF~tchVhj@ciCwhAzlC";
+ const decoded = parsePolylineInput(encoded);
+
+ expect(decoded).toHaveLength(EXPECTED_POINTS.length);
+ for (let i = 0; i < EXPECTED_POINTS.length; i++) {
+ expect(decoded[i].latitude).toBeCloseTo(EXPECTED_POINTS[i].latitude, 5);
+ expect(decoded[i].longitude).toBeCloseTo(EXPECTED_POINTS[i].longitude, 5);
+ }
+ });
+
+ test("decodes an S2 Polyline", () => {
+ const s2String =
+ "AQMAAAA7_tIhjf_avysne_M5iOW__WHh1yJy4z9MIcUK8Pvav7QbiLMRiuW_SWY5Y1lx4z-CIIuaN__av1Eb3-fUh-W_iPpHZ7By4z8=";
+ const decoded = parsePolylineInput(s2String);
+
+ expect(decoded).toHaveLength(EXPECTED_POINTS.length);
+ for (let i = 0; i < EXPECTED_POINTS.length; i++) {
+ expect(decoded[i].latitude).toBeCloseTo(EXPECTED_POINTS[i].latitude, 5);
+ expect(decoded[i].longitude).toBeCloseTo(EXPECTED_POINTS[i].longitude, 5);
+ }
+ });
+
+ test("decodes a GMP Unencoded Polyline (Plain Text)", () => {
+ const input =
+ "{ latitude: 37.42213, longitude: -122.0848 }, { latitude: 37.4152, longitude: -122.0627 }, { latitude: 37.427, longitude: -122.0854 }";
+ const decoded = parsePolylineInput(input);
+
+ expect(decoded).toHaveLength(EXPECTED_POINTS.length);
+ for (let i = 0; i < EXPECTED_POINTS.length; i++) {
+ expect(decoded[i].latitude).toBe(EXPECTED_POINTS[i].latitude);
+ expect(decoded[i].longitude).toBe(EXPECTED_POINTS[i].longitude);
+ }
+ });
+
+ test("decodes a JSON Polyline", () => {
+ const input = JSON.stringify(EXPECTED_POINTS);
+ const decoded = parsePolylineInput(input);
+
+ expect(decoded).toHaveLength(EXPECTED_POINTS.length);
+ for (let i = 0; i < EXPECTED_POINTS.length; i++) {
+ expect(decoded[i].latitude).toBe(EXPECTED_POINTS[i].latitude);
+ expect(decoded[i].longitude).toBe(EXPECTED_POINTS[i].longitude);
+ }
+ });
+
+ test("throws error on invalid input", () => {
+ expect(() => parsePolylineInput("not a polyline")).toThrow();
+ });
+});