Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utilities to convert GPS trace file formats into SnapToRoads request #44

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
"@aws-sdk/client-geo-routes": "^3.683.0",
"@aws-sdk/client-location": "^3.682.0",
"@aws/polyline": "^0.1.0",
"@here/flexpolyline": "^0.1.0",
"@turf/circle": "^6.5.0",
"@types/geojson": "^7946.0.14",
"csv-parse": "^5.5.6"
Expand Down
83 changes: 80 additions & 3 deletions src/from-csv/tracepoints-converter.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,100 @@
import { csvStringToRoadSnapTracePointList } from "./tracepoints-converter";

describe("csvToRoadSnapTracePointList", () => {
it("should convert csv string to RoadSnapTracePointList", () => {
it("should convert csv string with headers to RoadSnapTracePointList (speed_kmh)", () => {
const csvString = `latitude,longitude,speed_kmh,timestamp,heading
53.3737131,-1.4704939,12.5,2024-11-15T10:30:00Z,45
53.3742428,-1.4677477,15.8,2024-11-15T10:31:30Z,78`;
expect(csvStringToRoadSnapTracePointList(csvString)).toEqual([
{
Position: [-1.470494, 53.373713],
Position: [-1.4704939, 53.3737131],
Speed: 12.5,
Timestamp: "2024-11-15T10:30:00Z",
Heading: 45,
},
{
Position: [-1.467748, 53.374243],
Position: [-1.4677477, 53.3742428],
Speed: 15.8,
Timestamp: "2024-11-15T10:31:30Z",
Heading: 78,
},
]);
});

it("should handle custom column mapping", () => {
const csvString = `y,x,velocity,time,direction
53.3737131,-1.4704939,12.5,2024-11-15T10:30:00Z,45
53.3742428,-1.4677477,15.8,2024-11-15T10:31:30Z,78`;

const result = csvStringToRoadSnapTracePointList(csvString, {
columnMapping: {
latitude: "y",
longitude: "x",
speed_kmh: "velocity",
timestamp: "time",
heading: "direction",
},
});

expect(result).toEqual([
{
Position: [-1.4704939, 53.3737131],
Speed: 12.5,
Timestamp: "2024-11-15T10:30:00Z",
Heading: 45,
},
{
Position: [-1.4677477, 53.3742428],
Speed: 15.8,
Timestamp: "2024-11-15T10:31:30Z",
Heading: 78,
},
]);
});

it("should handle speed in m/s", () => {
const csvString = `latitude,longitude,speed_mps,timestamp,heading
53.3737131,-1.4704939,3.47222,2024-11-15T10:30:00Z,45
53.3742428,-1.4677477,4.38889,2024-11-15T10:31:30Z,78`;

const result = csvStringToRoadSnapTracePointList(csvString);

expect(result).toEqual([
{
Position: [-1.4704939, 53.3737131],
Speed: 12.499992,
Timestamp: "2024-11-15T10:30:00Z",
Heading: 45,
},
{
Position: [-1.4677477, 53.3742428],
Speed: 15.800004,
Timestamp: "2024-11-15T10:31:30Z",
Heading: 78,
},
]);
});

it("should handle speed in mph", () => {
const csvString = `latitude,longitude,speed_mph,timestamp,heading
53.3737131,-1.4704939,7.76713,2024-11-15T10:30:00Z,45
53.3742428,-1.4677477,9.81747,2024-11-15T10:31:30Z,78`;

const result = csvStringToRoadSnapTracePointList(csvString);

expect(result).toEqual([
{
Position: [-1.4704939, 53.3737131],
Speed: 12.4999529942,
Timestamp: "2024-11-15T10:30:00Z",
Heading: 45,
},
{
Position: [-1.4677477, 53.3742428],
Speed: 15.7996471698,
Timestamp: "2024-11-15T10:31:30Z",
Heading: 78,
},
]);
});
});
116 changes: 75 additions & 41 deletions src/from-csv/tracepoints-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,92 @@ import { RoadSnapTracePoint } from "@aws-sdk/client-geo-routes";
* It converts a CSV string to an array of RoadSnapTracePoint, so the result can be used to assemble the request to
* SnapToRoads API.
*
* @example Converting a CSV string
* Expected fields:
*
* Input:
* | Field | Required | Description |
* | --------- | -------- | ------------------------------------------------- |
* | latitude | Yes | Latitude in decimal degrees (e.g., 37.7749295) |
* | longitude | Yes | Longitude in decimal degrees (e.g., -122.4194239) |
* | speed_kmh | No | Speed in kilometers per hour |
* | speed_mps | No | Speed in meters per second |
* | speed_mph | No | Speed in miles per hour |
* | timestamp | No | ISO 8601 format (e.g., 2024-11-19T14:45:00Z) |
* | heading | No | Direction in degrees (0-360) |
*
* `latitude,longitude,speed_kmh,timestamp,heading 37.7749295,-122.4194239,18.3,2024-11-19T14:45:00Z,210
* 37.7750321,-122.4201567,22.6,2024-11-19T14:46:30Z,185`
* Note: If multiple speed fields are provided, speed_kmh takes precedence.
*
* Output:
* @example Basic usage const result = csvStringToRoadSnapTracePointList(csvString);
*
* ```json
* [
* {
* "Position": [-122.419424, 37.77493],
* "Timestamp": "2024-11-19T14:45:00Z",
* "Speed": 18.3,
* "Heading": 210
* },
* {
* "Position": [-122.420157, 37.775032],
* "Timestamp": "2024-11-19T14:46:30Z",
* "Speed": 22.6,
* "Heading": 185
* }
* ]
* ```
* @example With custom column mapping const result = csvStringToRoadSnapTracePointList(csvString, { columnMapping: {
* latitude: 'y', longitude: 'x' } });
*
* @param csvString - The input CSV string to be parsed.
HabibaaElsherbiny marked this conversation as resolved.
Show resolved Hide resolved
* @param options - Optional configuration for parsing.
* @param options.columnMapping - Object mapping expected column names to actual CSV column names.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param options.columnMapping - Object mapping expected column names to actual CSV column names.
* @param options.columnMapping - Object mapping expected column names to actual CSV column names. This uses column names provided in the header row.

(We could also support CSVs without header rows by accepting an array with column names as columnNames or similar.)

* @returns An array of RfoadSnapTracePoint objects.
HabibaaElsherbiny marked this conversation as resolved.
Show resolved Hide resolved
*/
export function csvStringToRoadSnapTracePointList(csvString: string) {
const records = parse(csvString, { columns: true, trim: true });

return records.map((row) => convertCSVToTracepoint(row));
type ColumnMapping = {
latitude?: string;
longitude?: string;
speed_kmh?: string;
speed_mps?: string;
speed_mph?: string;
timestamp?: string;
heading?: string;
};

interface ParseOptions {
columnMapping?: ColumnMapping;
}

export function csvStringToRoadSnapTracePointList(csvString: string, options: ParseOptions = {}): RoadSnapTracePoint[] {
const { columnMapping = {} } = options;

const records = parse(csvString, {
columns: true,
skip_empty_lines: true,
trim: true,
});

const effectiveColumnMapping = Object.keys(records[0]).reduce((acc, header) => {
const key = Object.keys(columnMapping).find((k) => columnMapping[k] === header) || header;
acc[key] = header;
return acc;
}, {});

return records.map((row) => convertCSVToTracepoint(row, effectiveColumnMapping));
}

function convertCSVToTracepoint(row): RoadSnapTracePoint | undefined {
const longitude = Math.round(parseFloat(row.longitude) * Math.pow(10, 6)) / Math.pow(10, 6);
const latitude = Math.round(parseFloat(row.latitude) * Math.pow(10, 6)) / Math.pow(10, 6);
function convertCSVToTracepoint(
row: Record<string, string>,
columnMapping: Record<string, string>,
): RoadSnapTracePoint {
const getValue = (key: string) => row[columnMapping[key]];

const longitude = parseFloat(getValue("longitude"));
const latitude = parseFloat(getValue("latitude"));

const roadSnapTracePoint: RoadSnapTracePoint = { Position: [longitude, latitude] };

const roadSnapTracePoint = { Position: [longitude, latitude] };
if (row.timestamp) {
roadSnapTracePoint["Timestamp"] = row.timestamp;
// Handle speed (only one type will be provided)
if (columnMapping.speed_kmh) {
roadSnapTracePoint.Speed = parseFloat(getValue("speed_kmh"));
} else if (columnMapping.speed_mps) {
roadSnapTracePoint.Speed = parseFloat(getValue("speed_mps")) * 3.6;
} else if (columnMapping.speed_mph) {
roadSnapTracePoint.Speed = parseFloat(getValue("speed_mph")) * 1.60934;
}
if (row.speed_kmh) {
const speedKMPH = row.speed_kmh;
roadSnapTracePoint["Speed"] = Math.round(speedKMPH * 100) / 100;
} else if (row.speed_mps) {
const speedKMPH = row.speed_mps * 3.6;
roadSnapTracePoint["Speed"] = Math.round(speedKMPH * 100) / 100;
} else if (row.speed_mph) {
const speedKMPH = row.speed_mph * 1.60934;
roadSnapTracePoint["Speed"] = Math.round(speedKMPH * 100) / 100;

const timestamp = getValue("timestamp");
if (timestamp) {
roadSnapTracePoint.Timestamp = timestamp;
}
if (row.heading) {
roadSnapTracePoint["Heading"] = parseFloat(row.heading);

const heading = getValue("heading");
if (heading) {
roadSnapTracePoint.Heading = parseFloat(heading);
}

return roadSnapTracePoint;
}
25 changes: 11 additions & 14 deletions src/from-flexible-polyline/tracepoints-converter.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { flexiblePolylineStringToRoadSnapTracePointList } from "./tracepoints-converter";
import { encodeFromLngLatArray } from "@aws/polyline";

describe("flexiblePolylineToRoadSnapTracePointList", () => {
it("should convert flexible polyline string to RoadSnapTracePointList", () => {
expect(flexiblePolylineStringToRoadSnapTracePointList("FP:BFoz5xJ67i1B1B7PzIhaxL7Y")).toEqual([
{
Position: [8.69821, 50.10228],
},
{
Position: [8.69567, 50.10201],
},
{
Position: [8.6915, 50.10063],
},
{
Position: [8.68752, 50.09878],
},
]);
const input = [
[8.69821, 50.10228],
[8.69567, 50.10201],
[8.6915, 50.10063],
[8.68752, 50.09878],
];
const encoded = encodeFromLngLatArray(input);
expect(flexiblePolylineStringToRoadSnapTracePointList(`FP:${encoded}`)).toEqual(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do our APIs require the FP: prefix? The API reference doesn't say anything about it, but that might be wrong.

input.map((coordinates) => ({ Position: coordinates })),
);
});
});
17 changes: 4 additions & 13 deletions src/from-flexible-polyline/tracepoints-converter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { decode } from "@here/flexpolyline";
import { decodeToLngLatArray } from "@aws/polyline";
import { RoadSnapTracePoint } from "@aws-sdk/client-geo-routes";

Check warning on line 5 in src/from-flexible-polyline/tracepoints-converter.ts

View workflow job for this annotation

GitHub Actions / build

'RoadSnapTracePoint' is defined but never used

/**
* It converts a Flexible Polyline string to an array of RoadSnapTracePoint, so the result can be used to assemble the
Expand All @@ -28,21 +28,12 @@
export function flexiblePolylineStringToRoadSnapTracePointList(fpString: string) {
if (fpString.startsWith("FP:")) {
const encodedString = fpString.slice(3);
const decodedString = decode(encodedString);
const decodedLngLatArray = decodeToLngLatArray(encodedString);

if (decodedString.polyline) {
return decodedString.polyline.map((coordinates) => convertCoordinatesToTracepoint(coordinates));
if (decodedLngLatArray) {
return decodedLngLatArray.map((coordinates) => ({ Position: coordinates }));
}
} else {
console.log("Invalid input: Flexible polyline string should start with 'FP:'");
}
}

function convertCoordinatesToTracepoint(coordinates): RoadSnapTracePoint {
const longitude = Math.round(coordinates[1] * Math.pow(10, 6)) / Math.pow(10, 6);
const latitude = Math.round(coordinates[0] * Math.pow(10, 6)) / Math.pow(10, 6);

return {
Position: [longitude, latitude],
};
}
2 changes: 1 addition & 1 deletion src/from-geojson/tracepoints-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("featureCollectionToRoadSnapTracePointList", () => {
{
Position: [8.53379056, 50.16352417],
Timestamp: "2019-08-20T15:13:27.512Z",
Speed: 2.34,
Speed: 2.3400000000000003,
Heading: 177.3,
},
{
Expand Down
38 changes: 32 additions & 6 deletions src/from-geojson/tracepoints-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
* It converts a FeatureCollection with Point Features to an array of RoadSnapTracePoint, so the result can be used to
* assemble the request to SnapToRoads API.
*
* @example Converting geojson tracepoints
* @remarks
* The function processes the following properties:
*
* - Timestamp_msec
* - Speed_mps, speed_kmh or speed_mph
* - Heading
HabibaaElsherbiny marked this conversation as resolved.
Show resolved Hide resolved
*
* Other properties that may be present in the input (such as provider, accuracy, and altitude) are ignored.
*
* Note: If multiple speed fields are provided, speed_kmh takes precedence.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be explicit about the full order in case speed_kmh is absent but the other 2 are present for whatever reason.

* @example Converting GeoJSON tracepoints
*
* Input:
*
Expand Down Expand Up @@ -69,11 +79,23 @@
* ]
* ```
*/
export function featureCollectionToRoadSnapTracePointList(featureCollection: FeatureCollection<Point>) {

type TracePointProperties = {
timestamp_msec?: number;
speed_mps?: number;
speed_kmh?: number;
speed_mph?: number;
heading?: number;
[key: string]: any; // This allows for additional properties

Check warning on line 89 in src/from-geojson/tracepoints-converter.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
};

export function featureCollectionToRoadSnapTracePointList(
featureCollection: FeatureCollection<Point, TracePointProperties>,
) {
return featureCollection.features.map((feature) => convertFeatureToTracepoint(feature));
}

function convertFeatureToTracepoint(feature: Feature<Point>): RoadSnapTracePoint | undefined {
function convertFeatureToTracepoint(feature: Feature<Point, TracePointProperties>): RoadSnapTracePoint | undefined {
if (feature) {
const roadSnapTracePoint = {
Position: feature.geometry.coordinates,
Expand All @@ -84,13 +106,17 @@
roadSnapTracePoint["Timestamp"] = timestamp.toISOString();
}

if (feature.properties.speed_mps) {
if (feature.properties.speed_kmh !== undefined) {
roadSnapTracePoint["Speed"] = feature.properties.speed_kmh;
} else if (feature.properties.speed_mps !== undefined) {
const speedKMPH = feature.properties.speed_mps * 3.6;
roadSnapTracePoint["Speed"] = Math.round(speedKMPH * 100) / 100;
roadSnapTracePoint["Speed"] = speedKMPH;
} else if (feature.properties.speed_mph !== undefined) {
roadSnapTracePoint["Speed"] = feature.properties.speed_mph * 1.60934;
}

if (feature.properties.heading) {
roadSnapTracePoint["Heading"] = parseFloat(feature.properties.heading);
roadSnapTracePoint["Heading"] = feature.properties.heading;
}

return roadSnapTracePoint;
Expand Down
2 changes: 1 addition & 1 deletion src/from-gpx/tracepoints-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("gpxToRoadSnapTracePointList", () => {
<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gte="http://www.gpstrackeditor.com/xmlschemas/General/1" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" targetNamespace="http://www.topografix.com/GPX/1/1" elementFormDefault="qualified" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>sample_rome.gpx</name>
<desc>Sample data collected in Rome: showing finding main roads, ignored waypoints, illegal one ways( near #45, #81), gate traversal(near #72), passing no-through-traffic zone (near #79)</desc>
<desc>Sample data</desc>
</metadata>
<trk>
<name>Rome</name>
Expand Down
Loading
Loading