Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}\"",
Expand Down
8 changes: 7 additions & 1 deletion src/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}, []);
Expand Down
48 changes: 4 additions & 44 deletions src/PolylineCreation.js
Original file line number Diff line number Diff line change
@@ -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("");
Expand All @@ -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 (
<div
Expand Down
109 changes: 109 additions & 0 deletions src/PolylineUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { decode as decodeS2 } from "s2polyline-ts";

/**
* Decodes a Google Maps encoded polyline string into an array of LatLng objects.
* Based on the Google Polyline Algorithm.
* @param {string} encoded
* @returns {Array<{latitude: number, longitude: number}>}
*/
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.");
}
59 changes: 59 additions & 0 deletions src/PolylineUtils.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading