{ "cells": [ { "cell_type": "markdown", "id": "3d0f2ad3", "metadata": {}, "source": [ "# Google Maps Integration" ] }, { "cell_type": "markdown", "id": "e3e91187", "metadata": {}, "source": [ "This notebook shows how the [Google Maps API](https://developers.google.com/maps/) is integrated to plot a courier's path from a restaurant to a customer." ] }, { "cell_type": "code", "execution_count": 1, "id": "3bb8099f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[32murban-meal-delivery\u001b[0m, version \u001b[34m0.4.0\u001b[0m\n" ] } ], "source": [ "!umd --version" ] }, { "cell_type": "markdown", "id": "5fbc6ccb", "metadata": {}, "source": [ "### Imports" ] }, { "cell_type": "code", "execution_count": 2, "id": "6b5c3483", "metadata": {}, "outputs": [], "source": [ "import googlemaps as gm\n", "\n", "from urban_meal_delivery import config, db" ] }, { "cell_type": "code", "execution_count": 3, "id": "3dd58c43", "metadata": {}, "outputs": [], "source": [ "%load_ext lab_black" ] }, { "cell_type": "markdown", "id": "5c7493c8", "metadata": {}, "source": [ "### Settings" ] }, { "cell_type": "markdown", "id": "2e76e261", "metadata": {}, "source": [ "Choose `\"Bordeaux\"`, `\"Lyon\"`, or `\"Paris\"`." ] }, { "cell_type": "code", "execution_count": 4, "id": "5c6fc1f0", "metadata": {}, "outputs": [], "source": [ "city_name = \"Paris\"" ] }, { "cell_type": "markdown", "id": "435f8cfb", "metadata": {}, "source": [ "### Load the Data" ] }, { "cell_type": "code", "execution_count": 5, "id": "ac59dc73", "metadata": {}, "outputs": [], "source": [ "city = db.session.query(db.City).filter_by(name=city_name).one()" ] }, { "cell_type": "code", "execution_count": 6, "id": "b15b36f2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "city" ] }, { "cell_type": "code", "execution_count": 7, "id": "6f76bece", "metadata": {}, "outputs": [], "source": [ "restaurants = (\n", " db.session.query(db.Restaurant)\n", " .join(db.Address)\n", " .filter(db.Address.city == city)\n", " .all()\n", ")" ] }, { "cell_type": "code", "execution_count": 8, "id": "7c3968be", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1153" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(restaurants)" ] }, { "cell_type": "markdown", "id": "baaf60b1", "metadata": {}, "source": [ "## Visualization" ] }, { "cell_type": "markdown", "id": "16176dc0", "metadata": {}, "source": [ "Let's choose a restaurant and then one of its orders." ] }, { "cell_type": "code", "execution_count": 9, "id": "baf34d52", "metadata": {}, "outputs": [], "source": [ "restaurant = restaurants[0]" ] }, { "cell_type": "code", "execution_count": 10, "id": "2e13f48d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3297" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(restaurant.orders)" ] }, { "cell_type": "code", "execution_count": 11, "id": "9c70a621", "metadata": {}, "outputs": [], "source": [ "order = restaurant.orders[10]" ] }, { "cell_type": "markdown", "id": "ff273184", "metadata": {}, "source": [ "Plot the courier's path from the restaurant to the customer's delivery address." ] }, { "cell_type": "code", "execution_count": 12, "id": "d91b07a5", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
Make this Notebook Trusted to load map: File -> Trust Notebook
" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "order.draw()" ] }, { "cell_type": "markdown", "id": "753ee9a5", "metadata": {}, "source": [ "## Behind the Scenes" ] }, { "cell_type": "markdown", "id": "2ae9598c", "metadata": {}, "source": [ "The code above integrates the [googlemaps](https://github.com/googlemaps/google-maps-services-python) PyPI package into the application running the UDP's routing optimization.\n", "\n", "Let's first look at how we can use the [googlemaps](https://github.com/googlemaps/google-maps-services-python) PyPI package directly to make indiviual API calls.\n", "\n", "Then, we'll see how this code is **abstracted** into the **application logic**." ] }, { "cell_type": "markdown", "id": "988ff8c5", "metadata": {}, "source": [ "### Direct Google Maps API Calls" ] }, { "cell_type": "markdown", "id": "722f9932", "metadata": {}, "source": [ "With an API key, one can create a new `client` object that handles all communication with the [Google Maps API](https://developers.google.com/maps/)." ] }, { "cell_type": "code", "execution_count": 13, "id": "174f6746", "metadata": {}, "outputs": [], "source": [ "client = gm.Client(config.GOOGLE_MAPS_API_KEY)" ] }, { "cell_type": "markdown", "id": "a02b9c5a", "metadata": {}, "source": [ "For example, we can **geocode** an address: This means that we obtain the **latitude-longitude** coordinates for a given *postal* address, and some other structured info." ] }, { "cell_type": "code", "execution_count": 14, "id": "4168a03c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'address_components': [{'long_name': '2',\n", " 'short_name': '2',\n", " 'types': ['street_number']},\n", " {'long_name': 'Burgplatz', 'short_name': 'Burgpl.', 'types': ['route']},\n", " {'long_name': 'Vallendar',\n", " 'short_name': 'Vallendar',\n", " 'types': ['locality', 'political']},\n", " {'long_name': 'Mayen-Koblenz',\n", " 'short_name': 'Mayen-Koblenz',\n", " 'types': ['administrative_area_level_3', 'political']},\n", " {'long_name': 'Rheinland-Pfalz',\n", " 'short_name': 'RP',\n", " 'types': ['administrative_area_level_1', 'political']},\n", " {'long_name': 'Germany',\n", " 'short_name': 'DE',\n", " 'types': ['country', 'political']},\n", " {'long_name': '56179', 'short_name': '56179', 'types': ['postal_code']}],\n", " 'formatted_address': 'Burgpl. 2, 56179 Vallendar, Germany',\n", " 'geometry': {'bounds': {'northeast': {'lat': 50.40070650000001,\n", " 'lng': 7.6142806},\n", " 'southwest': {'lat': 50.4001466, 'lng': 7.6128609}},\n", " 'location': {'lat': 50.40035899999999, 'lng': 7.613506600000001},\n", " 'location_type': 'ROOFTOP',\n", " 'viewport': {'northeast': {'lat': 50.4017755302915,\n", " 'lng': 7.614919730291503},\n", " 'southwest': {'lat': 50.3990775697085, 'lng': 7.612221769708499}}},\n", " 'place_id': 'ChIJjcydwDx9vkcR6sSdsYNtPOo',\n", " 'types': ['premise']}]" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.geocode(\"Burgplatz 2, Vallendar\")" ] }, { "cell_type": "markdown", "id": "dc2c1fe4", "metadata": {}, "source": [ "For routing applications, we are in particular interested in collecting all the *pair-wise* distances in a **distance matrix** for all involved **origins** and **destinations**." ] }, { "cell_type": "code", "execution_count": 15, "id": "961f6fc2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].address" ] }, { "cell_type": "code", "execution_count": 16, "id": "5444d517", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(48.85313, 2.37461)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].address.location.lat_lng" ] }, { "cell_type": "code", "execution_count": 17, "id": "7ca537e6", "metadata": {}, "outputs": [], "source": [ "origins = [\n", " restaurants[0].address.location.lat_lng,\n", " restaurants[1].address.location.lat_lng,\n", "]" ] }, { "cell_type": "code", "execution_count": 18, "id": "76eb7cb1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].orders[100].delivery_address" ] }, { "cell_type": "code", "execution_count": 19, "id": "f3c4674c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(48.864269, 2.385568)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].orders[100].delivery_address.location.lat_lng" ] }, { "cell_type": "code", "execution_count": 20, "id": "6a1dfee8", "metadata": {}, "outputs": [], "source": [ "destinations = [\n", " restaurants[0].orders[100].delivery_address.location.lat_lng,\n", " restaurants[1].orders[100].delivery_address.location.lat_lng,\n", "]" ] }, { "cell_type": "markdown", "id": "20dfd317", "metadata": {}, "source": [ "The [Google Maps API](https://developers.google.com/maps/) provides a `.distance_matrix()` method for that." ] }, { "cell_type": "code", "execution_count": 21, "id": "25575bfa", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'destination_addresses': ['83 Bd de Ménilmontant, 75011 Paris, France',\n", " '7 Rue des Abbesses, 75018 Paris, France'],\n", " 'origin_addresses': ['19 Rue de Charonne, 75011 Paris, France',\n", " '64 Rue Saint-Lazare, 75009 Paris, France'],\n", " 'rows': [{'elements': [{'distance': {'text': '2.4 km', 'value': 2389},\n", " 'duration': {'text': '12 mins', 'value': 701},\n", " 'status': 'OK'},\n", " {'distance': {'text': '5.8 km', 'value': 5798},\n", " 'duration': {'text': '24 mins', 'value': 1454},\n", " 'status': 'OK'}]},\n", " {'elements': [{'distance': {'text': '5.7 km', 'value': 5735},\n", " 'duration': {'text': '22 mins', 'value': 1332},\n", " 'status': 'OK'},\n", " {'distance': {'text': '1.2 km', 'value': 1211},\n", " 'duration': {'text': '10 mins', 'value': 574},\n", " 'status': 'OK'}]}],\n", " 'status': 'OK'}" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.distance_matrix(\n", " origins=origins,\n", " destinations=destinations,\n", " mode=\"bicycling\", # Choose an appropriate travelling mode\n", ")" ] }, { "cell_type": "markdown", "id": "be52a0f3", "metadata": {}, "source": [ "The `.directions()` method provides the **legs** (i.e., **waypoints**) of a **route** from *one* location to another." ] }, { "cell_type": "code", "execution_count": 22, "id": "5a5e31b5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'bounds': {'northeast': {'lat': 48.8643175, 'lng': 2.3887503},\n", " 'southwest': {'lat': 48.8530147, 'lng': 2.3745434}},\n", " 'copyrights': 'Map data ©2021',\n", " 'legs': [{'distance': {'text': '2.0 km', 'value': 2008},\n", " 'duration': {'text': '6 mins', 'value': 358},\n", " 'end_address': '19 Rue de Charonne, 75011 Paris, France',\n", " 'end_location': {'lat': 48.8530828, 'lng': 2.3745434},\n", " 'start_address': '83 Bd de Ménilmontant, 75011 Paris, France',\n", " 'start_location': {'lat': 48.8643175, 'lng': 2.3856677},\n", " 'steps': [{'distance': {'text': '0.6 km', 'value': 554},\n", " 'duration': {'text': '2 mins', 'value': 98},\n", " 'end_location': {'lat': 48.85983239999999, 'lng': 2.3887503},\n", " 'html_instructions': 'Head southeast on Bd de Ménilmontant toward Rue de Tlemcen/Rue Spinoza',\n", " 'polyline': {'points': '_yfiHm}pMt@{@`D{DJMBCh@m@HGDCLEjJsD`@MPGB?`@QjCaA'},\n", " 'start_location': {'lat': 48.8643175, 'lng': 2.3856677},\n", " 'travel_mode': 'BICYCLING'},\n", " {'distance': {'text': '0.8 km', 'value': 750},\n", " 'duration': {'text': '2 mins', 'value': 127},\n", " 'end_location': {'lat': 48.8573962, 'lng': 2.379628},\n", " 'html_instructions': 'Turn right onto Rue de la Roquette
Go through 1 roundabout
',\n", " 'maneuver': 'turn-right',\n", " 'polyline': {'points': '}|eiHupqMTrA\\\\pBHd@TtADZ@NBN\\\\xBHp@BJ`@~B`@~BNz@^~B@FRjAf@xCP|@Fd@RhA@N@JHh@@NBNBNBV@N?H?TA^A@A??@A@A@?@A@EJ?BAB?B?B?B?@?B@J@@?B@B?@@B@@@B@@@@@@@@@?@@@@@?@?@?@?@?@?@??A@?@?@ADFBFBF?BL`@BBBB\\\\P'},\n", " 'start_location': {'lat': 48.85983239999999, 'lng': 2.3887503},\n", " 'travel_mode': 'BICYCLING'},\n", " {'distance': {'text': '0.5 km', 'value': 488},\n", " 'duration': {'text': '2 mins', 'value': 96},\n", " 'end_location': {'lat': 48.853281, 'lng': 2.3773315},\n", " 'html_instructions': 'Continue onto Av. Ledru Rollin',\n", " 'polyline': {'points': 'wmeiHuwoMh@XPFRJB@B?DAn@XHDl@Z`ChAHDFDxBdA`@R`@Rd@V`DxAz@^ZJ'},\n", " 'start_location': {'lat': 48.8573962, 'lng': 2.379628},\n", " 'travel_mode': 'BICYCLING'},\n", " {'distance': {'text': '0.2 km', 'value': 205},\n", " 'duration': {'text': '1 min', 'value': 35},\n", " 'end_location': {'lat': 48.8530147, 'lng': 2.374655},\n", " 'html_instructions': 'Turn right onto Rue de Charonne',\n", " 'maneuver': 'turn-right',\n", " 'polyline': {'points': '_tdiHiioM?~@@~@Av@ALAhAAn@?T?L?H@N@H@NJd@BLFNDNFPHNDH'},\n", " 'start_location': {'lat': 48.853281, 'lng': 2.3773315},\n", " 'travel_mode': 'BICYCLING'},\n", " {'distance': {'text': '11 m', 'value': 11},\n", " 'duration': {'text': '1 min', 'value': 2},\n", " 'end_location': {'lat': 48.8530828, 'lng': 2.3745434},\n", " 'html_instructions': 'Turn right onto Cr du Panier Fleuri
Destination will be on the right
',\n", " 'maneuver': 'turn-right',\n", " 'polyline': {'points': 'irdiHqxnMMT'},\n", " 'start_location': {'lat': 48.8530147, 'lng': 2.374655},\n", " 'travel_mode': 'BICYCLING'}],\n", " 'traffic_speed_entry': [],\n", " 'via_waypoint': []}],\n", " 'overview_polyline': {'points': '_yfiHm}pMfFiGr@u@RIjJsD`@MTGlDsAr@dEf@fDn@fEbEfVZnBBZRxADf@?^C`@A@EFGVDb@LRLBDAB?FDTt@d@XrAn@HAx@^`EpBbFdC|ExBZJ?~@?vBCvAA|ABXLt@X~@NXMT'},\n", " 'summary': 'Bd de Ménilmontant and Rue de la Roquette',\n", " 'warnings': [\"Bicycling directions are in beta. Use caution – This route may contain streets that aren't suited for bicycling.\"],\n", " 'waypoint_order': []}]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.directions(\n", " destination=restaurants[0].address.location.lat_lng,\n", " origin=restaurants[0].orders[100].delivery_address.location.lat_lng,\n", " mode=\"bicycling\", # Choose an appropriate travelling mode\n", ")" ] }, { "cell_type": "markdown", "id": "e22404f4", "metadata": {}, "source": [ "### Abstraction as `Path` Objects" ] }, { "cell_type": "markdown", "id": "fb6a035d", "metadata": {}, "source": [ "In the application's code base, the above API calls and the related data are modeled as `Path` objects connecting two `Address` objects (cf. [Path class](https://github.com/webartifex/urban-meal-delivery/blob/main/src/urban_meal_delivery/db/addresses_addresses.py) in the code).\n", "\n", "Let's look at two examples addresses, one from a `Restaurant` and one from a `Customer`." ] }, { "cell_type": "code", "execution_count": 23, "id": "23e5552c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].address" ] }, { "cell_type": "code", "execution_count": 24, "id": "80ff4e0f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "restaurants[0].orders[100].delivery_address" ] }, { "cell_type": "markdown", "id": "754b5f16", "metadata": {}, "source": [ "The `Path.from_addresses()` constructor method takes any number of `Address` objects and creates all entries of a *symmetric* **distance matrix** as `Path` objects.\n", "\n", "Here, we only get *one* `Path` object as there are only two `Address` objects." ] }, { "cell_type": "code", "execution_count": 25, "id": "15b16871", "metadata": {}, "outputs": [], "source": [ "paths = db.Path.from_addresses(\n", " restaurants[0].address, restaurants[0].orders[100].delivery_address\n", ")\n", "\n", "path = paths[0]" ] }, { "cell_type": "code", "execution_count": 26, "id": "9554fd8e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path" ] }, { "cell_type": "markdown", "id": "84bf8534", "metadata": {}, "source": [ "As we assume a *generic* and **symmetric** distance matrix, we call the two `Address` objects \"first\" and \"second\" and not \"restaurant\" and \"customer\". After all, a `Customer` may live in a house that has a `Restaurant` on the ground floor." ] }, { "cell_type": "code", "execution_count": 27, "id": "82f04cf4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.first_address" ] }, { "cell_type": "code", "execution_count": 28, "id": "fc081362", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.second_address" ] }, { "cell_type": "markdown", "id": "9af847e4", "metadata": {}, "source": [ "Because we have `.latitude`-`.longitue` values for each `Address`, we can calculate the path's `.air_distance` even *without* talking to the Google Maps API." ] }, { "cell_type": "code", "execution_count": 29, "id": "b4fbcca9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1475" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.air_distance" ] }, { "cell_type": "markdown", "id": "e99d0883", "metadata": {}, "source": [ "The `Path.sync_with_google_maps()` method loads all the data needed from Google but makes sure that we do not make another API call if we already have the data." ] }, { "cell_type": "code", "execution_count": 30, "id": "564fa98a", "metadata": {}, "outputs": [], "source": [ "path.sync_with_google_maps()" ] }, { "cell_type": "markdown", "id": "d22864f7", "metadata": {}, "source": [ "Google provides `.bicycle_distance` (in meters) and `.bicylce_duration` (in seconds) approximations for a courier's path from one location to another." ] }, { "cell_type": "code", "execution_count": 31, "id": "c2532b98", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2389" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.bicycle_distance" ] }, { "cell_type": "code", "execution_count": 32, "id": "b3b69c75", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "702" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.bicycle_duration" ] }, { "cell_type": "markdown", "id": "2339190e", "metadata": {}, "source": [ "In addition, the above `\"legs\"` values are stored as proper UTM coordinates for convenient plotting." ] }, { "cell_type": "code", "execution_count": 33, "id": "1745b42e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path.waypoints" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" } }, "nbformat": 4, "nbformat_minor": 5 }