diff --git a/11_classes/01_exercises_solved.ipynb b/11_classes/01_exercises_solved.ipynb new file mode 100644 index 0000000..3b4334f --- /dev/null +++ b/11_classes/01_exercises_solved.ipynb @@ -0,0 +1,2534 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/develop?urlpath=lab/tree/11_classes/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 11: Classes & Instances (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/develop/11_classes/00_content.ipynb) of Chapter 11.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Berlin Tourist Guide: A Traveling Salesman Problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is a hands-on and tutorial-like application to show how to load data from web services like [Google Maps](https://developers.google.com/maps) and use them to solve a logistics problem, namely a **[Traveling Salesman Problem ](https://en.wikipedia.org/wiki/Traveling_salesman_problem)**.\n", + "\n", + "Imagine that a tourist lands at Berlin's [Tegel Airport ](https://en.wikipedia.org/wiki/Berlin_Tegel_Airport) in the morning and has his \"connecting\" flight from [Schönefeld Airport ](https://en.wikipedia.org/wiki/Berlin_Sch%C3%B6nefeld_Airport) in the evening. By the time, the flights were scheduled, the airline thought that there would be only one airport in Berlin.\n", + "\n", + "Having never been in Berlin before, the tourist wants to come up with a plan of sights that he can visit with a rental car on his way from Tegel to Schönefeld.\n", + "\n", + "With a bit of research, he creates a `list` of `sights` like below." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "arrival = \"Berlin Tegel Airport (TXL), Berlin\"\n", + "\n", + "sights = [\n", + " \"Alexanderplatz, Berlin\",\n", + " \"Brandenburger Tor, Pariser Platz, Berlin\",\n", + " \"Checkpoint Charlie, Friedrichstraße, Berlin\",\n", + " \"Kottbusser Tor, Berlin\",\n", + " \"Mauerpark, Berlin\",\n", + " \"Siegessäule, Berlin\",\n", + " \"Reichstag, Platz der Republik, Berlin\",\n", + " \"Soho House Berlin, Torstraße, Berlin\",\n", + " \"Tempelhofer Feld, Berlin\",\n", + "]\n", + "\n", + "departure = \"Berlin Schönefeld Airport (SXF), Berlin\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With just the street addresses, however, he cannot calculate a route. He needs `latitude`-`longitude` coordinates instead. While he could just open a site like [Google Maps](https://www.google.com/maps) in a web browser, he wonders if he can download the data with a bit of Python code using a [web API ](https://en.wikipedia.org/wiki/Web_API) offered by [Google](https://www.google.com).\n", + "\n", + "So, in this notebook, we solve the entire problem with code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Geocoding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to obtain coordinates for the given street addresses above, a process called **geocoding**, we use the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start).\n", + "\n", + "**Q1**: Familiarize yourself with this [documentation](https://developers.google.com/maps/documentation/geocoding/start), register a developer account, create a project, and [create an API key](https://console.cloud.google.com/apis/credentials) that is necessary for everything to work! Then, [enable the Geocoding API](https://console.developers.google.com/apis/library/geocoding-backend.googleapis.com) and link a [billing account](https://console.developers.google.com/billing)!\n", + "\n", + "Info: The first 200 Dollars per month are not charged (cf., [pricing page](https://cloud.google.com/maps-platform/pricing/)), so no costs will incur for this tutorial. You must sign up because Google simply wants to know the people using its services.\n", + "\n", + "**Q2**: Assign the API key as a `str` object to the `key` variable!" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "key = \" < your API key goes here > \"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To use external web services, our application needs to make HTTP requests just like web browsers do when surfing the web.\n", + "\n", + "We do not have to implement this on our own. Instead, we use the official Python Client for the Google Maps Services provided by Google in one of its corporate [GitHub repositories ](https://github.com/googlemaps).\n", + "\n", + "**Q3**: Familiarize yourself with the [googlemaps ](https://github.com/googlemaps/google-maps-services-python) package! Then, install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: googlemaps in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (4.4.2)\n", + "Requirement already satisfied: requests<3.0,>=2.20.0 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from googlemaps) (2.24.0)\n", + "Requirement already satisfied: idna<3,>=2.5 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (2.10)\n", + "Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (1.25.11)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (2020.6.20)\n", + "Requirement already satisfied: chardet<4,>=3.0.2 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests<3.0,>=2.20.0->googlemaps) (3.0.4)\n" + ] + } + ], + "source": [ + "!pip install googlemaps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Finish the following code cells and instantiate a `Client` object named `api`! Use the `key` from above. `api` provides us with a lot of methods to talk to the API." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import googlemaps" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "api = googlemaps.Client(key=key)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "api" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "googlemaps.client.Client" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(api)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Execute the next code cell to list the methods and attributes on the `api` object!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['base_url',\n", + " 'channel',\n", + " 'clear_experience_id',\n", + " 'client_id',\n", + " 'client_secret',\n", + " 'directions',\n", + " 'distance_matrix',\n", + " 'elevation',\n", + " 'elevation_along_path',\n", + " 'find_place',\n", + " 'geocode',\n", + " 'geolocate',\n", + " 'get_experience_id',\n", + " 'key',\n", + " 'nearest_roads',\n", + " 'place',\n", + " 'places',\n", + " 'places_autocomplete',\n", + " 'places_autocomplete_query',\n", + " 'places_nearby',\n", + " 'places_photo',\n", + " 'queries_per_second',\n", + " 'requests_kwargs',\n", + " 'retry_over_query_limit',\n", + " 'retry_timeout',\n", + " 'reverse_geocode',\n", + " 'sent_times',\n", + " 'session',\n", + " 'set_experience_id',\n", + " 'snap_to_roads',\n", + " 'snapped_speed_limits',\n", + " 'speed_limits',\n", + " 'static_map',\n", + " 'timeout',\n", + " 'timezone']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[x for x in dir(api) if not x.startswith(\"_\")]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To obtain all kinds of information associated with a street address, we call the `geocode()` method with the address as the sole argument.\n", + "\n", + "For example, let's search for Brandenburg Gate. Its street address is `\"Brandenburger Tor, Pariser Platz, Berlin\"`.\n", + "\n", + "**Q6**: Execute the next code cell!\n", + "\n", + "Hint: If you get an error message, follow the instructions in it to debug it.\n", + "\n", + "If everything works, we receive a `list` with a single `dict` in it. That means the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) only knows about one place at the address. Unfortunately, the `dict` is pretty dense and hard to read." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'address_components': [{'long_name': 'Pariser Platz',\n", + " 'short_name': 'Pariser Platz',\n", + " 'types': ['route']},\n", + " {'long_name': 'Mitte',\n", + " 'short_name': 'Mitte',\n", + " 'types': ['political', 'sublocality', 'sublocality_level_1']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['locality', 'political']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['administrative_area_level_1', 'political']},\n", + " {'long_name': 'Germany',\n", + " 'short_name': 'DE',\n", + " 'types': ['country', 'political']},\n", + " {'long_name': '10117', 'short_name': '10117', 'types': ['postal_code']}],\n", + " 'formatted_address': 'Pariser Platz, 10117 Berlin, Germany',\n", + " 'geometry': {'location': {'lat': 52.5162746, 'lng': 13.3777041},\n", + " 'location_type': 'GEOMETRIC_CENTER',\n", + " 'viewport': {'northeast': {'lat': 52.51762358029149,\n", + " 'lng': 13.3790530802915},\n", + " 'southwest': {'lat': 52.5149256197085, 'lng': 13.3763551197085}}},\n", + " 'place_id': 'ChIJiQnyVcZRqEcRY0xnhE77uyY',\n", + " 'plus_code': {'compound_code': 'G98H+G3 Berlin, Germany',\n", + " 'global_code': '9F4MG98H+G3'},\n", + " 'types': ['establishment', 'point_of_interest', 'tourist_attraction']}]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Capture the first and only search result in the `brandenburg_gate` variable and \"pretty print\" it with the help of the [pprint() ](https://docs.python.org/3/library/pprint.html#pprint.pprint) function in the [pprint ](https://docs.python.org/3/library/pprint.html) module in the [standard library ](https://docs.python.org/3/library/index.html)!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "response = api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = response[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `dict` has several keys that are of use for us: `\"formatted_address\"` is a cleanly formatted version of the address. `\"geometry\"` is a nested `dict` with several `lat`-`lng` coordinates representing the place where `\"location\"` is the one we need for our calculations. Lastly, `\"place_id\"` is a unique identifier that allows us to obtain further information about the address from other Google APIs." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'address_components': [{'long_name': 'Pariser Platz',\n", + " 'short_name': 'Pariser Platz',\n", + " 'types': ['route']},\n", + " {'long_name': 'Mitte',\n", + " 'short_name': 'Mitte',\n", + " 'types': ['political',\n", + " 'sublocality',\n", + " 'sublocality_level_1']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['locality', 'political']},\n", + " {'long_name': 'Berlin',\n", + " 'short_name': 'Berlin',\n", + " 'types': ['administrative_area_level_1', 'political']},\n", + " {'long_name': 'Germany',\n", + " 'short_name': 'DE',\n", + " 'types': ['country', 'political']},\n", + " {'long_name': '10117',\n", + " 'short_name': '10117',\n", + " 'types': ['postal_code']}],\n", + " 'formatted_address': 'Pariser Platz, 10117 Berlin, Germany',\n", + " 'geometry': {'location': {'lat': 52.5162746, 'lng': 13.3777041},\n", + " 'location_type': 'GEOMETRIC_CENTER',\n", + " 'viewport': {'northeast': {'lat': 52.51762358029149,\n", + " 'lng': 13.3790530802915},\n", + " 'southwest': {'lat': 52.5149256197085,\n", + " 'lng': 13.3763551197085}}},\n", + " 'place_id': 'ChIJiQnyVcZRqEcRY0xnhE77uyY',\n", + " 'plus_code': {'compound_code': 'G98H+G3 Berlin, Germany',\n", + " 'global_code': '9F4MG98H+G3'},\n", + " 'types': ['establishment', 'point_of_interest', 'tourist_attraction']}\n" + ] + } + ], + "source": [ + "pprint(brandenburg_gate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To keep our code readable and maintainable, we create a `Place` class to manage the API results in a clean way.\n", + "\n", + "The `.__init__()` method takes a `street_address` (e.g., an element of `sights`) and a `client` argument (e.g., an object like `api`) and stores them on `self`. The place's `.name` is parsed out of the `street_address` as well: It is the part before the first comma. Also, the instance attributes `.latitude`, `.longitude`, and `.place_id` are initialized to `None`.\n", + "\n", + "**Q8**: Finish the `.__init__()` method according to the description!\n", + "\n", + "The `.sync_from_google()` method uses the internally kept `client` and synchronizes the place's state with the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start). In particular, it updates the `.address` with the `formatted_address` and stores the values for `.latitude`, `.longitude`, and `.place_id`. It enables method chaining.\n", + "\n", + "**Q9**: Implement the `.sync_from_google()` method according to the description!\n", + "\n", + "**Q10**: Add a read-only `.location` property on the `Place` class that returns the `.latitude` and `.longitude` as a `tuple`!" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answer to Q8\n", + " def __init__(self, street_address, *, client):\n", + " \"\"\"Create a new place.\n", + "\n", + " Args:\n", + " street_address (str): street address of the place\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " \"\"\"\n", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\n", + "\n", + " def __repr__(self):\n", + " cls, name = self.__class__.__name__, self.name\n", + " synced = \" [SYNCED]\" if self.place_id else \"\"\n", + " return f\"<{cls}: {name}{synced}>\"\n", + "\n", + " # answer to Q9\n", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " # answer to Q10\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Verify that the instantiating a `Place` object works!" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: What do the angle brackets `<` and `>` mean in the text representation?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can obtain the geo-data from the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) in a clean way. As we enabled method chaining for `.sync_from_google()`, we get back the instance after calling the method.\n", + "\n", + "**Q13**: Verify that the `.sync_from_google()` method works!" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Pariser Platz, 10117 Berlin, Germany'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.address" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'ChIJiQnyVcZRqEcRY0xnhE77uyY'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.place_id" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(52.5162746, 13.3777041)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.location" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Batch Synchronization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Add an alternative constructor method named `.from_addresses()` that takes an `addresses`, a `client`, and a `sync` argument! `addresses` is a finite iterable of `str` objects (e.g., like `sights`). The method returns a `list` of `Place`s, one for each `str` in `addresses`. All `Place`s are initialized with the same `client`. `sync` is a flag and defaults to `False`. If it is set to `True`, the alternative constructor invokes the `.sync_from_google()` method on the `Place`s before returning them." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "code_folding": [] + }, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + " def __init__(self, street_address, *, client):\n", + " \"\"\"Create a new place.\n", + "\n", + " Args:\n", + " street_address (str): street address of the place\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " \"\"\"\n", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\n", + "\n", + " def __repr__(self):\n", + " cls, name = self.__class__.__name__, self.name\n", + " synced = \" [SYNCED]\" if self.place_id else \"\"\n", + " return f\"<{cls}: {name}{synced}>\"\n", + "\n", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " # answer to Q14\n", + " @classmethod\n", + " def from_addresses(cls, addresses, *, client, sync=False):\n", + " \"\"\"Create new places in a batch.\n", + "\n", + " Args:\n", + " addresses (iterable of str's): the street addresses of the places\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " Returns:\n", + " list of Places\n", + " \"\"\"\n", + " places = []\n", + " for address in addresses:\n", + " place = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\n", + " return places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q15**: Verify that the alternative constructor works with and without the `sync` flag!" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Place.from_addresses(sights, client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For geo-data it always makes sense to plot them on a map. We use the third-party library [folium ](https://github.com/python-visualization/folium) to achieve that.\n", + "\n", + "**Q16**: Familiarize yourself with [folium ](https://github.com/python-visualization/folium) and install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: folium in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (0.11.0)\n", + "Requirement already satisfied: requests in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (2.24.0)\n", + "Requirement already satisfied: jinja2>=2.9 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (2.11.2)\n", + "Requirement already satisfied: branca>=0.3.0 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (0.4.1)\n", + "Requirement already satisfied: numpy in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from folium) (1.19.2)\n", + "Requirement already satisfied: idna<3,>=2.5 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (2.10)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (2020.6.20)\n", + "Requirement already satisfied: chardet<4,>=3.0.2 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (3.0.4)\n", + "Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from requests->folium) (1.25.11)\n", + "Requirement already satisfied: MarkupSafe>=0.23 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from jinja2>=2.9->folium) (1.1.1)\n" + ] + } + ], + "source": [ + "!pip install folium" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q17**: Execute the code cells below to create an empty map of Berlin!" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "import folium" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = folium.Map(location=(52.513186, 13.3944349), zoom_start=14)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "folium.folium.Map" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(berlin)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`folium.Map` instances are shown as interactive maps in Jupyter notebooks whenever they are the last expression in a code cell." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Make this Notebook Trusted to load map: File -> Trust Notebook" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to put something on the map, [folium ](https://github.com/python-visualization/folium) works with so-called `Marker` objects.\n", + "\n", + "**Q18**: Review its docstring and then create a marker `m` with the location data of Brandenburg Gate! Use the `brandenburg_gate` object from above!\n", + "\n", + "Hint: You may want to use HTML tags for the `popup` argument to format the text output on the map in a nicer way. So, instead of just passing `\"Brandenburger Tor\"` as the `popup` argument, you could use, for example, `\"Brandenburger Tor(Pariser Platz, 10117 Berlin, Germany)\"`. Then, the name appears in bold and the street address is put on the next line. You could use an f-string to parametrize the argument." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mfolium\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mMarker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlocation\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpopup\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtooltip\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0micon\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdraggable\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Create a simple stock Leaflet marker on the map, with optional\n", + "popup text or Vincent visualization.\n", + "\n", + "Parameters\n", + "----------\n", + "location: tuple or list\n", + " Latitude and Longitude of Marker (Northing, Easting)\n", + "popup: string or folium.Popup, default None\n", + " Label for the Marker; either an escaped HTML string to initialize\n", + " folium.Popup or a folium.Popup instance.\n", + "tooltip: str or folium.Tooltip, default None\n", + " Display a text when hovering over the object.\n", + "icon: Icon plugin\n", + " the Icon plugin to use to render the marker.\n", + "draggable: bool, default False\n", + " Set to True to be able to drag the marker around the map.\n", + "\n", + "Returns\n", + "-------\n", + "Marker names and HTML in obj.template_vars\n", + "\n", + "Examples\n", + "--------\n", + ">>> Marker(location=[45.5, -122.3], popup='Portland, OR')\n", + ">>> Marker(location=[45.5, -122.3], popup=Popup('Portland, OR'))\n", + "# If the popup label has characters that need to be escaped in HTML\n", + ">>> Marker(location=[45.5, -122.3],\n", + "... popup=Popup('Mom & Pop Arrow Shop >>', parse_html=True))\n", + "\u001b[0;31mFile:\u001b[0m ~/repos/intro-to-python/.venv/lib/python3.8/site-packages/folium/map.py\n", + "\u001b[0;31mType:\u001b[0m type\n", + "\u001b[0;31mSubclasses:\u001b[0m Circle, CircleMarker, RegularPolygonMarker\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "folium.Marker?" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "m = folium.Marker(\n", + " location=brandenburg_gate.location,\n", + " popup=f\"{brandenburg_gate.name}({brandenburg_gate.address})\",\n", + " tooltip=brandenburg_gate.name,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "folium.map.Marker" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q19**: Execute the next code cells that add `m` to the `berlin` map!" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.add_to(berlin)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Make this Notebook Trusted to load map: File -> Trust Notebook" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Marker Representation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q20**: Finish the `.as_marker()` method that returns a `Marker` instance when invoked on a `Place` instance! The method takes an optional `color` argument that uses [folium ](https://github.com/python-visualization/folium)'s `Icon` type to control the color of the marker." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + " def __init__(self, street_address, *, client):\n", + " \"\"\"Create a new place.\n", + "\n", + " Args:\n", + " street_address (str): street address of the place\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " \"\"\"\n", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\n", + "\n", + " def __repr__(self):\n", + " cls, name = self.__class__.__name__, self.name\n", + " synced = \" [SYNCED]\" if self.place_id else \"\"\n", + " return f\"<{cls}: {name}{synced}>\"\n", + "\n", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " @classmethod\n", + " def from_addresses(cls, addresses, *, client, sync=False):\n", + " \"\"\"Create new places in a batch.\n", + "\n", + " Args:\n", + " addresses (iterable of str's): the street addresses of the places\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " Returns:\n", + " list of Places\n", + " \"\"\"\n", + " places = []\n", + " for address in addresses:\n", + " place = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\n", + " return places\n", + "\n", + " # answer to Q20\n", + " def as_marker(self, *, color=\"blue\"):\n", + " \"\"\"Create a Marker representation of the place.\n", + "\n", + " Args:\n", + " color (str): color of the marker, defaults to \"blue\"\n", + "\n", + " Returns:\n", + " marker (folium.Marker)\n", + "\n", + " Raises:\n", + " RuntimeError: if the place is not synchronized with\n", + " the Google Maps Geocoding API\n", + " \"\"\"\n", + " if not self.place_id:\n", + " raise RuntimeError(\"must synchronize with Google first\")\n", + " return folium.Marker(\n", + " location=self.location,\n", + " popup=f\"{self.name}({self.address})\",\n", + " tooltip=self.name,\n", + " icon=folium.Icon(color=color),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q21**: Execute the next code cells that create a new `Place` and obtain a `Marker` for it!\n", + "\n", + "Notes: Without synchronization, we get a `RuntimeError`. `.as_marker()` can be chained right after `.sync_from_google()`" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "must synchronize with Google first", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mbrandenburg_gate\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mas_marker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mas_marker\u001b[0;34m(self, color)\u001b[0m\n\u001b[1;32m 69\u001b[0m \"\"\"\n\u001b[1;32m 70\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplace_id\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 71\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"must synchronize with Google first\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 72\u001b[0m return folium.Marker(\n\u001b[1;32m 73\u001b[0m \u001b[0mlocation\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlocation\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mRuntimeError\u001b[0m: must synchronize with Google first" + ] + } + ], + "source": [ + "brandenburg_gate.as_marker()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "brandenburg_gate.sync_from_google().as_marker()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q22**: Use the alternative `.from_addresses()` constructor to create a `list` named `places` with already synced `Place`s!" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "subslide" + } + }, + "source": [ + "To make [folium ](https://github.com/python-visualization/folium)'s `Map` class work even better with our `Place` instances, we write our own `Map` class wrapping [folium ](https://github.com/python-visualization/folium)'s. Then, we add further functionality to the class throughout this tutorial.\n", + "\n", + "The `.__init__()` method takes mandatory `name`, `center`, `start`, `end`, and `places` arguments. `name` is there for convenience, `center` is the map's initial center, `start` and `end` are `Place` instances, and `places` is a finite iterable of `Place` instances. Also, `.__init__()` accepts an optional `initial_zoom` argument defaulting to `12`.\n", + "\n", + "Upon initialization, a `folium.Map` instance is created and stored as an implementation detail `_map`. Also, `.__init__()` puts markers for each place on the `_map` object: `\"green\"` and `\"red\"` markers for the `start` and `end` locations and `\"blue\"` ones for the `places` to be visited. To do that, `.__init__()` invokes another `.add_marker()` method on the `Map` class, once for every `Place` object. `.add_marker()` itself invokes the `.add_to()` method on the `folium.Marker` representation of a `Place` instance and enables method chaining.\n", + "\n", + "To keep the state in a `Map` instance consistent, all passed in arguments except `name` are treated as implementation details. Otherwise, a user of the `Map` class could, for example, change the `start` attribute, which would not be reflected in the internally kept `folium.Map` object.\n", + "\n", + "**Q23**: Implement the `.__init__()` and `.add_marker()` methods on the `Map` class as described!\n", + "\n", + "**Q24**: Add a `.show()` method on the `Map` class that simply returns the internal `folium.Map` object!" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answer to Q23\n", + " def __init__(self, name, center, start, end, places, initial_zoom=12):\n", + " \"\"\"Create a new map.\n", + "\n", + " Args:\n", + " name (str): name of the map\n", + " center (float, float): coordinates of the map's center\n", + " start (Place): start of the tour\n", + " end (Place): end of the tour\n", + " places (iterable of Places): the places to be visitied\n", + " initial_zoom (integer): zoom level according to folium's\n", + " specifications; defaults to 12\n", + " \"\"\"\n", + " self.name = name\n", + " self._start = start\n", + " self._end = end\n", + " self._places = places\n", + " self._map = folium.Map(location=center, zoom_start=initial_zoom)\n", + "\n", + " # Add markers to the map.\n", + " self.add_marker(start.as_marker(color=\"green\"))\n", + " self.add_marker(end.as_marker(color=\"red\"))\n", + " for place in places:\n", + " self.add_marker(place.as_marker())\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " # answer to Q24\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return self._map\n", + "\n", + " # answer to Q23\n", + " def add_marker(self, marker):\n", + " \"\"\"Add a marker to the map.\n", + "\n", + " Args:\n", + " marker (folium.Marker): marker to be put on the map\n", + " \"\"\"\n", + " marker.add_to(self._map)\n", + " return self" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's put all the sights, the two airports, and three more places, the [Bundeskanzleramt ](https://en.wikipedia.org/wiki/German_Chancellery), the [Olympic Stadium ](https://en.wikipedia.org/wiki/Olympiastadion_%28Berlin%29), and the [East Side Gallery ](https://en.wikipedia.org/wiki/East_Side_Gallery), on the map.\n", + "\n", + "**Q25**: Execute the next code cells to create a map of Berlin with all the places on it!\n", + "\n", + "Note: Because we implemented method chaining everywhere, the code below is only *one* expression written over several lines. It almost looks like a self-explanatory and compact \"language\" on its own." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = (\n", + " Map(\n", + " \"Sights in Berlin\",\n", + " center=(52.5015154, 13.4066838),\n", + " start=Place(arrival, client=api).sync_from_google(),\n", + " end=Place(departure, client=api).sync_from_google(),\n", + " places=places,\n", + " initial_zoom=10,\n", + " )\n", + " .add_marker(\n", + " Place(\"Bundeskanzleramt, Willy-Brandt-Straße, Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + " .add_marker(\n", + " Place(\"Olympiastadion Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + " .add_marker(\n", + " Place(\"East Side Gallery, Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Make this Notebook Trusted to load map: File -> Trust Notebook" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "berlin.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Distance Matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we can find out the best order in which to visit all the sights, we must calculate the pairwise distances between all points. While Google also offers a [Directions API](https://developers.google.com/maps/documentation/directions/start) and a [Distance Matrix API](https://developers.google.com/maps/documentation/distance-matrix/start), we choose to calculate the air distances using the third-party library [geopy ](https://github.com/geopy/geopy).\n", + "\n", + "**Q26**: Familiarize yourself with the [documentation](https://geopy.readthedocs.io/en/stable/) and install [geopy ](https://github.com/geopy/geopy) with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: geopy in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (2.0.0)\n", + "Requirement already satisfied: geographiclib<2,>=1.49 in /home/webartifex/repos/intro-to-python/.venv/lib/python3.8/site-packages (from geopy) (1.50)\n" + ] + } + ], + "source": [ + "!pip install geopy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use [geopy ](https://github.com/geopy/geopy) primarily for converting the `latitude`-`longitude` coordinates into a [distance matrix ](https://en.wikipedia.org/wiki/Distance_matrix).\n", + "\n", + "Because the [earth is not flat ](https://en.wikipedia.org/wiki/Flat_Earth), [geopy ](https://github.com/geopy/geopy) provides a `great_circle()` function that calculates the so-called [orthodromic distance ](https://en.wikipedia.org/wiki/Great-circle_distance) between two places on a sphere." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "from geopy.distance import great_circle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q27**: For quick reference, read the docstring of `great_circle()` and execute the code cells below to calculate the distance between the `arrival` and the `departure`!" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mInit signature:\u001b[0m \u001b[0mgreat_circle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Use spherical geometry to calculate the surface distance between two\n", + "points.\n", + "\n", + "Set which radius of the earth to use by specifying a ``radius`` keyword\n", + "argument. It must be in kilometers. The default is to use the module\n", + "constant `EARTH_RADIUS`, which uses the average great-circle radius.\n", + "\n", + "Example::\n", + "\n", + " >>> from geopy.distance import great_circle\n", + " >>> newport_ri = (41.49008, -71.312796)\n", + " >>> cleveland_oh = (41.499498, -81.695391)\n", + " >>> print(great_circle(newport_ri, cleveland_oh).miles)\n", + " 536.997990696\n", + "\u001b[0;31mFile:\u001b[0m ~/repos/intro-to-python/.venv/lib/python3.8/site-packages/geopy/distance.py\n", + "\u001b[0;31mType:\u001b[0m ABCMeta\n", + "\u001b[0;31mSubclasses:\u001b[0m \n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "great_circle?" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "tegel = Place(arrival, client=api).sync_from_google()\n", + "schoenefeld = Place(departure, client=api).sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Distance(25.370448366418135)" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "great_circle(tegel.location, schoenefeld.location)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25.370448366418135" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "great_circle(tegel.location, schoenefeld.location).km" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25370.448366418135" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "great_circle(tegel.location, schoenefeld.location).meters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Distance to another `Place`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q28**: Finish the `distance_to()` method in the `Place` class that takes a `other` argument and returns the distance in meters! Adhere to the given docstring!" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + " def __init__(self, street_address, *, client):\n", + " \"\"\"Create a new place.\n", + "\n", + " Args:\n", + " street_address (str): street address of the place\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " \"\"\"\n", + " self.name = street_address.split(\",\")[0]\n", + " self.address = street_address\n", + " self.client = client\n", + " self.latitude = None\n", + " self.longitude = None\n", + " self.place_id = None\n", + "\n", + " def __repr__(self):\n", + " cls, name = self.__class__.__name__, self.name\n", + " synced = \" [SYNCED]\" if self.place_id else \"\"\n", + " return f\"<{cls}: {name}{synced}>\"\n", + "\n", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = self.client.geocode(self.address)\n", + " first_hit = response[0]\n", + " self.address = first_hit[\"formatted_address\"]\n", + " self.latitude = first_hit[\"geometry\"][\"location\"][\"lat\"]\n", + " self.longitude = first_hit[\"geometry\"][\"location\"][\"lng\"]\n", + " self.place_id = first_hit[\"place_id\"]\n", + " return self\n", + "\n", + " @property\n", + " def location(self):\n", + " return self.latitude, self.longitude\n", + "\n", + " @classmethod\n", + " def from_addresses(cls, addresses, *, client, sync=False):\n", + " \"\"\"Create new places in a batch.\n", + "\n", + " Args:\n", + " addresses (iterable of str's): the street addresses of the places\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " Returns:\n", + " list of Places\n", + " \"\"\"\n", + " places = []\n", + " for address in addresses:\n", + " place = cls(address, client=client)\n", + " if sync:\n", + " place.sync_from_google()\n", + " places.append(place)\n", + " return places\n", + "\n", + " def as_marker(self, color=\"blue\"):\n", + " \"\"\"Create a Marker representation of the place.\n", + "\n", + " Args:\n", + " color (str): color of the marker, defaults to \"blue\"\n", + "\n", + " Returns:\n", + " marker (folium.Marker)\n", + "\n", + " Raises:\n", + " RuntimeError: if the place is not synchronized with\n", + " the Google Maps Geocoding API\n", + " \"\"\"\n", + " if not self.place_id:\n", + " raise RuntimeError(\"must synchronize with Google first\")\n", + " return folium.Marker(\n", + " location=self.location,\n", + " popup=f\"{self.name}({self.address})\",\n", + " tooltip=self.name,\n", + " icon=folium.Icon(color=color),\n", + " )\n", + "\n", + " # answer to Q28\n", + " def distance_to(self, other):\n", + " \"\"\"Calculate the distance to another place in meters.\n", + "\n", + " Args:\n", + " other (Place): the other place to calculate the distance to\n", + "\n", + " Returns:\n", + " distance (int)\n", + "\n", + " Raises:\n", + " RuntimeError: if one of the places is not synchronized with\n", + " the Google Maps Geocoding API\n", + " \"\"\"\n", + " if not self.place_id or not other.place_id:\n", + " raise RuntimeError(\"must synchronize places with Google first\")\n", + " return int(great_circle(self.location, other.location).meters)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q29**: Execute the code cells below to test the new feature!\n", + "\n", + "Note: If done right, object-oriented code reads almost like plain English." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "tegel = Place(arrival, client=api).sync_from_google()\n", + "schoenefeld = Place(departure, client=api).sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "25370" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tegel.distance_to(schoenefeld)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q30**: Execute the next code cell to instantiate the `Place`s in `sights` again!" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class (continued): Pairwise Distances" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we add a read-only `distances` property on our `Map` class. As we are working with air distances, these are *symmetric* which reduces the number of distances we must calculate.\n", + "\n", + "To do so, we use the [combinations() ](https://docs.python.org/3/library/itertools.html#itertools.combinations) generator function in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](https://docs.python.org/3/library/index.html). That produces all possible `r`-`tuple`s from an `iterable` argument. `r` is `2` in our case as we are looking at `origin`-`destination` pairs.\n", + "\n", + "Let's first look at an easy example of [combinations() ](https://docs.python.org/3/library/itertools.html#itertools.combinations) to understand how it works: It gives us all the `2`-`tuple`s from a `list` of five `numbers` disregarding the order of the `tuple`s' elements." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "import itertools" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2\n", + "1 3\n", + "1 4\n", + "1 5\n", + "2 3\n", + "2 4\n", + "2 5\n", + "3 4\n", + "3 5\n", + "4 5\n" + ] + } + ], + "source": [ + "numbers = [1, 2, 3, 4, 5]\n", + "\n", + "for x, y in itertools.combinations(numbers, 2):\n", + " print(x, y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`distances` uses the internal `._start`, `._end`, and `._places` attributes and creates a `dict` with the keys consisting of all pairs of `Place`s and the values being their distances in meters. As this operation is rather costly, we cache the distances the first time we calculate them into a hidden instance attribute `._distances`, which must be initialized with `None` in the `.__init__()` method.\n", + "\n", + "**Q31**: Finish the `.distances` property as described!" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answers from above with a tiny adjustment\n", + " def __init__(self, name, center, start, end, places, initial_zoom=12):\n", + " \"\"\"Create a new map.\n", + "\n", + " Args:\n", + " name (str): name of the map\n", + " center (float, float): coordinates of the map's center\n", + " start (Place): start of the tour\n", + " end (Place): end of the tour\n", + " places (iterable of Places): the places to be visitied\n", + " initial_zoom (integer): zoom level according to folium's\n", + " specifications; defaults to 12\n", + " \"\"\"\n", + " self.name = name\n", + " self._start = start\n", + " self._end = end\n", + " self._places = places\n", + " self._map = folium.Map(location=center, zoom_start=initial_zoom)\n", + " self._distances = None\n", + "\n", + " self.add_marker(start.as_marker(color=\"green\"))\n", + " self.add_marker(end.as_marker(color=\"red\"))\n", + " for place in places:\n", + " self.add_marker(place.as_marker())\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return self._map\n", + "\n", + " def add_marker(self, marker):\n", + " \"\"\"Add a marker to the map.\n", + "\n", + " Args:\n", + " marker (folium.Marker): marker to be put on the map\n", + " \"\"\"\n", + " marker.add_to(self._map)\n", + " return self\n", + "\n", + " # answer to Q31\n", + " @property\n", + " def distances(self):\n", + " \"\"\"Return a dict with the pairwise distances of all places.\n", + "\n", + " Implementation note: The results of the calculations are cached.\n", + " \"\"\"\n", + " if not self._distances:\n", + " distances = {}\n", + " all_pairs = itertools.combinations(\n", + " [self._start, self._end] + self._places,\n", + " r=2,\n", + " )\n", + " for origin, destination in all_pairs:\n", + " distance = origin.distance_to(destination)\n", + " distances[origin, destination] = distance\n", + " distances[destination, origin] = distance\n", + " self._distances = distances\n", + " return self._distances" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We pretty print the total distance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = Map(\n", + " \"Berlin\",\n", + " center=(52.5015154, 13.4066838),\n", + " start=Place(arrival, client=api).sync_from_google(),\n", + " end=Place(departure, client=api).sync_from_google(),\n", + " places=places,\n", + " initial_zoom=10,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{(, ): 7671,\n", + " (, ): 4654,\n", + " (, ): 5589,\n", + " (, ): 1821,\n", + " (, ): 9386,\n", + " (, ): 25370,\n", + " (, ): 9386,\n", + " (, ): 25370,\n", + " (, ): 4654,\n", + " (, ): 5878,\n", + " (, ): 2839,\n", + " (, ): 5112,\n", + " (, ): 8955,\n", + " (, ): 7430,\n", + " (, ): 7981,\n", + " (, ): 11020,\n", + " (, ): 7671,\n", + " (, ): 280,\n", + " (, ): 11020,\n", + " (, ): 6454,\n", + " (, ): 7981,\n", + " (, ): 6454,\n", + " (, ): 9266,\n", + " (, ): 7430,\n", + " (, ): 12042,\n", + " (, ): 9266,\n", + " (, ): 17695,\n", + " (, ): 12042,\n", + " (, ): 18133,\n", + " (, ): 17695,\n", + " (, ): 16861,\n", + " (, ): 18133,\n", + " (, ): 15199,\n", + " (, ): 16861,\n", + " (, ): 20057,\n", + " (, ): 15199,\n", + " (, ): 18944,\n", + " (, ): 20057,\n", + " (, ): 18411,\n", + " (, ): 18944,\n", + " (, ): 18230,\n", + " (, ): 18411,\n", + " (, ): 13382,\n", + " (, ): 18230,\n", + " (, ): 2491,\n", + " (, ): 13382,\n", + " (, ): 2240,\n", + " (, ): 2491,\n", + " (, ): 2577,\n", + " (, ): 2240,\n", + " (, ): 2368,\n", + " (, ): 2577,\n", + " (, ): 4354,\n", + " (, ): 2368,\n", + " (, ): 2539,\n", + " (, ): 4354,\n", + " (, ): 642,\n", + " (, ): 2539,\n", + " (, ): 5237,\n", + " (, ): 642,\n", + " (, ): 1304,\n", + " (, ): 5237,\n", + " (, ): 3353,\n", + " (, ): 1304,\n", + " (, ): 3383,\n", + " (, ): 3353,\n", + " (, ): 1876,\n", + " (, ): 3383,\n", + " (, ): 1876,\n", + " (, ): 2848,\n", + " (, ): 280,\n", + " (, ): 4831,\n", + " (, ): 2848,\n", + " (, ): 2110,\n", + " (, ): 4831,\n", + " (, ): 3984,\n", + " (, ): 2110,\n", + " (, ): 2837,\n", + " (, ): 3984,\n", + " (, ): 1571,\n", + " (, ): 2837,\n", + " (, ): 2811,\n", + " (, ): 1571,\n", + " (, ): 3648,\n", + " (, ): 2811,\n", + " (, ): 4930,\n", + " (, ): 3648,\n", + " (, ): 4928,\n", + " (, ): 4930,\n", + " (, ): 3590,\n", + " (, ): 4928,\n", + " (, ): 3185,\n", + " (, ): 3590,\n", + " (, ): 2851,\n", + " (, ): 3185,\n", + " (, ): 4749,\n", + " (, ): 2851,\n", + " (, ): 3219,\n", + " (, ): 4749,\n", + " (, ): 1838,\n", + " (, ): 3219,\n", + " (, ): 7451,\n", + " (