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

Adding Mapbox provider #8

Merged
merged 4 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

This tool compares the travel times obtained from [TravelTime Routes API](https://docs.traveltime.com/api/reference/routes),
[Google Maps Directions API](https://developers.google.com/maps/documentation/directions/get-directions),
and [TomTom Routing API](https://developer.tomtom.com/routing-api/documentation/tomtom-maps/routing-service).
[TomTom Routing API](https://developer.tomtom.com/routing-api/documentation/tomtom-maps/routing-service)
and [Mapbox Directions API](https://docs.mapbox.com/api/navigation/directions/).
Source code is available on [GitHub](https://github.com/traveltime-dev/traveltime-google-comparison).

## Features

- Get travel times from TravelTime API, Google Maps API and TomTom API in parallel, for provided origin/destination pairs and a set
- Get travel times from TravelTime API, Google Maps API, TomTom API and Mapbox in parallel, for provided origin/destination pairs and a set
of departure times.
- Departure times are calculated based on user provided start time, end time and interval.
- Analyze the differences between the results and print out the average error percentage.
Expand Down Expand Up @@ -47,6 +48,12 @@ For TomTom API:
export TOMTOM_API_KEY=[Your TomTom API Key]
```

For Mapbox API:

```bash
export MAPBOX_API_KEY=[Your Mapbox API Key]
```

For TravelTime API:
```bash
export TRAVELTIME_APP_ID=[Your TravelTime App ID]
Expand Down Expand Up @@ -87,6 +94,8 @@ Optional arguments:
It is enforced on per-second basis, to avoid bursts.
- `--tomtom-max-rpm [int]`: Set max number of parallel requests sent to TomTom API per minute. Default is 60.
It is enforced on per-second basis, to avoid bursts.
- `--mapbox-max-rpm [int]`: Set max number of parallel requests sent to Mapbox API per minute. Default is 60.
It is enforced on per-second basis, to avoid bursts.
- `--traveltime-max-rpm [int]`: Set max number of parallel requests sent to TravelTime API per minute. Default is 60.
It is enforced on per-second basis, to avoid bursts.

Expand Down
5 changes: 5 additions & 0 deletions src/traveltime_google_comparison/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

GOOGLE_API = "google"
TOMTOM_API = "tomtom"
MAPBOX_API = "mapbox"
TRAVELTIME_API = "traveltime"


Expand All @@ -23,6 +24,8 @@ def get_capitalized_provider_name(provider: str) -> str:
return "Google"
elif provider == "tomtom":
return "TomTom"
elif provider == "mapbox":
return "Mapbox"
elif provider == "traveltime":
return "TravelTime"
else:
Expand All @@ -37,6 +40,7 @@ class Fields:
TRAVEL_TIME = {
GOOGLE_API: "google_travel_time",
TOMTOM_API: "tomtom_travel_time",
MAPBOX_API: "mapbox_travel_time",
TRAVELTIME_API: "tt_travel_time",
}

Expand Down Expand Up @@ -143,6 +147,7 @@ async def collect_travel_times(
{
Fields.TRAVEL_TIME[GOOGLE_API]: "first",
Fields.TRAVEL_TIME[TOMTOM_API]: "first",
Fields.TRAVEL_TIME[MAPBOX_API]: "first",
Fields.TRAVEL_TIME[TRAVELTIME_API]: "first",
}
)
Expand Down
17 changes: 17 additions & 0 deletions src/traveltime_google_comparison/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

DEFAULT_GOOGLE_RPM = 60
DEFAULT_TOMTOM_RPM = 60
DEFAULT_MAPBOX_RPM = 60
DEFAULT_TRAVELTIME_RPM = 60

GOOGLE_API_KEY_VAR_NAME = "GOOGLE_API_KEY"
TOMTOM_API_KEY_VAR_NAME = "TOMTOM_API_KEY"
MAPBOX_API_KEY_VAR_NAME = "MAPBOX_API_KEY"
TRAVELTIME_APP_ID_VAR_NAME = "TRAVELTIME_APP_ID"
TRAVELTIME_API_KEY_VAR_NAME = "TRAVELTIME_API_KEY"

Expand Down Expand Up @@ -57,6 +59,13 @@ def parse_args():
default=DEFAULT_TOMTOM_RPM,
help="Maximum number of requests sent to TomTom API per minute",
)
parser.add_argument(
"--mapbox-max-rpm",
required=False,
type=int,
default=DEFAULT_MAPBOX_RPM,
help="Maximum number of requests sent to Mapbox API per minute",
)
parser.add_argument(
"--traveltime-max-rpm",
required=False,
Expand Down Expand Up @@ -92,6 +101,14 @@ def retrieve_tomtom_api_key():
return tomtom_api_key


def retrieve_mapbox_api_key():
mapbox_api_key = os.environ.get(MAPBOX_API_KEY_VAR_NAME)

if not mapbox_api_key:
raise ValueError(f"{MAPBOX_API_KEY_VAR_NAME} not set in environment variables.")
return mapbox_api_key


def retrieve_traveltime_credentials() -> TravelTimeCredentials:
app_id = os.environ.get(TRAVELTIME_APP_ID_VAR_NAME)
api_key = os.environ.get(TRAVELTIME_API_KEY_VAR_NAME)
Expand Down
10 changes: 8 additions & 2 deletions src/traveltime_google_comparison/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from traveltime_google_comparison import config
from traveltime_google_comparison.analysis import run_analysis
from traveltime_google_comparison.collect import (
MAPBOX_API,
Fields,
GOOGLE_API,
TRAVELTIME_API,
Expand All @@ -24,7 +25,7 @@


async def run():
providers = [GOOGLE_API, TOMTOM_API]
providers = [GOOGLE_API, TOMTOM_API, MAPBOX_API]
args = config.parse_args()
csv = pd.read_csv(
args.input, usecols=[Fields.ORIGIN, Fields.DESTINATION]
Expand All @@ -35,7 +36,10 @@ async def run():
return

request_handlers = factory.initialize_request_handlers(
args.google_max_rpm, args.tomtom_max_rpm, args.traveltime_max_rpm
args.google_max_rpm,
args.tomtom_max_rpm,
args.mapbox_max_rpm,
args.traveltime_max_rpm,
)
if args.skip_data_gathering:
travel_times_df = pd.read_csv(
Expand All @@ -46,6 +50,7 @@ async def run():
Fields.DEPARTURE_TIME,
Fields.TRAVEL_TIME[GOOGLE_API],
Fields.TRAVEL_TIME[TOMTOM_API],
Fields.TRAVEL_TIME[MAPBOX_API],
Fields.TRAVEL_TIME[TRAVELTIME_API],
],
)
Expand All @@ -56,6 +61,7 @@ async def run():
filtered_travel_times_df = travel_times_df.loc[
travel_times_df[Fields.TRAVEL_TIME[GOOGLE_API]].notna()
& travel_times_df[Fields.TRAVEL_TIME[TOMTOM_API]].notna()
& travel_times_df[Fields.TRAVEL_TIME[MAPBOX_API]].notna()
& travel_times_df[Fields.TRAVEL_TIME[TRAVELTIME_API]].notna(),
:,
]
Expand Down
13 changes: 11 additions & 2 deletions src/traveltime_google_comparison/requests/factory.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
from typing import Dict

from traveltime_google_comparison.collect import TOMTOM_API, TRAVELTIME_API, GOOGLE_API
from traveltime_google_comparison.collect import (
MAPBOX_API,
TOMTOM_API,
TRAVELTIME_API,
GOOGLE_API,
)
from traveltime_google_comparison.config import (
retrieve_google_api_key,
retrieve_mapbox_api_key,
retrieve_tomtom_api_key,
retrieve_traveltime_credentials,
)
from traveltime_google_comparison.requests.base_handler import BaseRequestHandler
from traveltime_google_comparison.requests.google_handler import GoogleRequestHandler
from traveltime_google_comparison.requests.tomtom_handler import TomTomRequestHandler
from traveltime_google_comparison.requests.mapbox_handler import MapboxRequestHandler
from traveltime_google_comparison.requests.traveltime_handler import (
TravelTimeRequestHandler,
)


def initialize_request_handlers(
google_max_rpm, tomtom_max_rpm, traveltime_max_rpm
google_max_rpm, tomtom_max_rpm, mapbox_max_rpm, traveltime_max_rpm
) -> Dict[str, BaseRequestHandler]:
google_api_key = retrieve_google_api_key()
tomtom_api_key = retrieve_tomtom_api_key()
mapbox_api_key = retrieve_mapbox_api_key()
credentials = retrieve_traveltime_credentials()
return {
GOOGLE_API: GoogleRequestHandler(google_api_key, google_max_rpm),
TOMTOM_API: TomTomRequestHandler(tomtom_api_key, tomtom_max_rpm),
MAPBOX_API: MapboxRequestHandler(mapbox_api_key, mapbox_max_rpm),
TRAVELTIME_API: TravelTimeRequestHandler(
credentials.app_id, credentials.api_key, traveltime_max_rpm
),
Expand Down
76 changes: 76 additions & 0 deletions src/traveltime_google_comparison/requests/mapbox_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging
from datetime import datetime

import aiohttp
from aiolimiter import AsyncLimiter
from traveltimepy import Coordinates

from traveltime_google_comparison.config import Mode
from traveltime_google_comparison.requests.base_handler import (
BaseRequestHandler,
RequestResult,
)

logger = logging.getLogger(__name__)


class MapboxApiError(Exception):
pass


class MapboxRequestHandler(BaseRequestHandler):
MAPBOX_ROUTES_URL = "https://api.mapbox.com/directions/v5/mapbox"

default_timeout = aiohttp.ClientTimeout(total=60)

def __init__(self, api_key, max_rpm):
self.api_key = api_key
self._rate_limiter = AsyncLimiter(max_rpm // 60, 1)

async def send_request(
self,
origin: Coordinates,
destination: Coordinates,
departure_time: datetime,
mode: Mode = Mode.DRIVING,
) -> RequestResult:
route = f"{origin.lng},{origin.lat};{destination.lng},{destination.lat}" # for Mapbox lat/lng are flipped!
transport_mode = get_mapbox_specific_mode(mode)
params = {
"depart_at": departure_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
"access_token": self.api_key,
"exclude": "ferry", # by default I think it includes ferries, but for our API we use just driving, without ferries
}
try:
async with aiohttp.ClientSession(
timeout=self.default_timeout
) as session, session.get(
f"{self.MAPBOX_ROUTES_URL}/{transport_mode}/{route}", params=params
) as response:
data = await response.json()
code = data["code"]
if code == "Ok":
duration = data["routes"][0]["duration"]
if not duration:
raise MapboxApiError(
"No route found between origin and destination."
)
return RequestResult(travel_time=int(duration))
else:
error_message = data.get("detailedError", "")
logger.error(
f"Error in Mapbox API response: {response.status} - {error_message}"
)
return RequestResult(None)
except Exception as e:
logger.error(f"Exception during requesting Mapbox API, {e}")
return RequestResult(None)


def get_mapbox_specific_mode(mode: Mode) -> str:
if mode == Mode.DRIVING:
return "driving-traffic"
elif mode == Mode.PUBLIC_TRANSPORT:
raise ValueError("Public transport is not supported for Mapbox requests")
Comment on lines +72 to +73
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PT is a bit problematic (not just with mapbox, but in general), skipping it for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't have an env var allowing the user to specify transport mode for now anyways

else:
raise ValueError(f"Unsupported mode: `{mode.value}`")
Loading