Interactive maps on Leaflet =========================== Whenever you go into a website that has some kind of interactive map, it is quite probable that you are wittnessing a map that has been made with a JavaScipt library called `Leaflet `_ (the other popular one that you might have wittnessed is called `OpenLayers `_). There is also a Python module called `Folium `_ that makes it possible visualize data that's been manipulated in Python on an interactive Leaflet map. Creating a simple interactive web-map ------------------------------------- Let's first see how we can do a simple interactive web-map without any data on it. We just visualize OpenStreetMap on a specific location of the a world. - First thing that we need to do is to create a Map instance. There are few parameters that we can use to adjust how in our Map instance that will affect how the background map will look like. We should already be able to see what our map looks like. .. code:: python import folium # Create a Map instance m = folium.Map(location=[58.37, 26.72], zoom_start=11, control_scale=True) m The first parameter ``location`` takes a pair of lat, lon values as list as an input which will determine where the map will be positioned when user opens up the map. ``zoom_start`` -parameter adjusts the default zoom-level for the map (the higher the number the closer the zoom is). ``control_scale`` defines if map should have a scalebar or not. .. ipython:: python :suppress: import folium import gdal import geopandas as gpd from fiona.crs import from_epsg # Create a Map instance m = folium.Map(location=[58.37, 26.72], zoom_start=11, control_scale=True) - We can already save the map without any content. It will now just show the basemap in such a way that we initialized it. Let's save the map as ``Data/base_map.html``. .. ipython:: python :suppress: import os outfp = os.path.join(os.path.abspath("data"), "base_map.html") m.save(outfp) .. code:: python # Filepath to the output outfp = r"Data\base_map.html" # Save the map m.save(outfp) Take a look at the map by clicking it with right mouse and open it with Google Chrome which then opens it up in a web browser. - Let's change the basemap style to ``Stamen Toner`` and change the location of our map slightly. The ``tiles`` -parameter is used for changing the background map provider and map style (see here for all possible ones). .. code:: python # Let's change the basemap style to 'Stamen Toner' m = folium.Map(location=[58.37, 26.72], tiles='Stamen Toner', zoom_start=12, control_scale=True, prefer_canvas=True) m - And in order to save the file: .. code:: python # Filepath to the output outfp = r"Data\base_map2.html" # Save the map m.save(outfp) .. ipython:: python :suppress: m = folium.Map(location=[58.37, 26.72], tiles='Stamen Toner', zoom_start=12, control_scale=True, prefer_canvas=True) outfp = os.path.join(os.path.abspath("data"), "base_map2.html") m.save(outfp) .. admonition:: Task Play around with the parameters and save the map and see how those changes affect the look of the map. Adding layers to the map ------------------------ Adding layers to a web-map is fairly straightforward and similar procedure as with Bokeh and we can use familiar tools to handle the data, i.e. Geopandas. Our ultimate aim is to create a plot like this where population in Tartumaa, road network and the schools are plotted on top of a web-map: .. raw:: html
First we need to prepare the data. .. code:: python from fiona.crs import from_epsg from shapely.geometry import LineString, MultiLineString # Filepaths grid_fp = r"L5_Data\population_square_km.shp" roads_fp = r"L5_Data\roads.shp" schools_fp = r"L5_Data\schools_tartu.shp" # Read files grid = gpd.read_file(grid_fp) roads = gpd.read_file(roads_fp) schools = gpd.read_file(schools_fp) # Re-project to WGS84 grid['geometry'] = grid['geometry'].to_crs(epsg=4326) roads['geometry'] = roads['geometry'].to_crs(epsg=4326) schools['geometry'] = schools['geometry'].to_crs(epsg=4326) # Make a selection (only data above 0 and below 1000) grid = grid.loc[(grid['Population'] > 0)] # Create a Geo-id which is needed by the Folium (it needs to have a unique identifier for each row) grid['geoid'] = grid.index.astype(str) schools['geoid'] = schools.index.astype(str) # Select data grid = grid[['geoid', 'Population', 'geometry']] # Save the file as geojson jsontxt = grid.to_json() Now we have our population data stored in the ``jsontxt`` variable as GeoJSON format which basically contains the data as text in a similar way that it would be written in a ``.geojson`` -file. Now we can start visualizing our data with Folium. .. code:: python from folium.plugins import MarkerCluster map_osm = folium.Map(location=[58.37, 26.72], tiles='Stamen toner', zoom_start=8, control_scale=True, prefer_canvas=True, width=600, height=450) # Create a Clustered map where points are clustered marker_cluster = MarkerCluster().add_to(map_osm) # Create Choropleth map where the colors are coming from a column "Population". # Notice: 'geoid' column that we created earlier needs to be assigned always as the first column # with threshold_scale we can adjust the class intervals for the values map_osm.choropleth(geo_data=jsontxt, data=grid, columns=['geoid', 'Population'], key_on="feature.id", fill_color='YlOrRd', fill_opacity=0.5, line_opacity=0.2, line_color='white', line_weight=0, threshold_scale=[0, 5, 10, 100, 1000], legend_name='Population in Tartu', highlight=False, smooth_factor=1.0) def getLinesAsPointList(row, geom): """Returns a list of coordinate pair tuples for the line ('lat', 'lon') of a LineString geometry""" if isinstance(row[geom], MultiLineString): return [] else: list_x = list(row[geom].coords.xy[0]) list_y = list(row[geom].coords.xy[1]) # we need lat lon order for the folium map!!! return list(zip(list_y, list_x)) # Calculate x and y coordinates of the line roads['points_list'] = roads.apply(getLinesAsPointList, geom='geometry', axis=1) # list of lat lon coordinate pair tuples roadpoints = list(roads['points_list']) for road in roadpoints: folium.PolyLine(road, color="grey", weight=2.5, opacity=1).add_to(map_osm) # Create Schools points on top of the map for idx, row in schools.iterrows(): # Get lat and lon of points lon = row['geometry'].x lat = row['geometry'].y # Get schools name information names = row['name'] # Add marker to the map folium.RegularPolygonMarker(location=[lat, lon], popup=names, fill_color='#2b8cbe', number_of_sides=6, radius=8).add_to(marker_cluster) # Save the output outfp = r'Data\pop15.html' map_osm.save(outfp) That's it! Now we have a cool interactive map with some markers on it and grid showing the population in the Tartumaa Region on top of a basemap. Open it with your browser and see the result. .. ipython:: python :suppress: import folium import gdal import os import geopandas as gpd from fiona.crs import from_epsg from shapely.geometry import LineString, MultiLineString def getLinesAsPointList(row, geom): """Returns a list of coordinate pair tuples for the line ('lat', 'lon') of a LineString geometry""" if isinstance(row[geom], MultiLineString): return [] else: list_x = list(row[geom].coords.xy[0]) list_y = list(row[geom].coords.xy[1]) # we need lat lon order for the folium map!!! return list(zip(list_y, list_x)) .. ipython:: python :suppress: import folium import gdal import os import geopandas as gpd from fiona.crs import from_epsg from shapely.geometry import LineString, MultiLineString from folium.plugins import MarkerCluster map_osm = folium.Map(location=[58.37, 26.72], tiles='Stamen toner', zoom_start=8, control_scale=True, prefer_canvas=True, width=600, height=450) # Create a Clustered map where points are clustered marker_cluster = MarkerCluster().add_to(map_osm) grid_fp = os.path.join(os.path.abspath('data'), "L5_Data/population_square_km.shp") roads_fp = os.path.join(os.path.abspath('data'), "L5_Data/roads.shp") schools_fp = os.path.join(os.path.abspath('data'), "L5_Data/schools_tartu.shp") grid = gpd.read_file(grid_fp) roads = gpd.read_file(roads_fp) schools = gpd.read_file(schools_fp) grid['geometry'] = grid['geometry'].to_crs(epsg=4326) roads['geometry'] = roads['geometry'].to_crs(epsg=4326) schools['geometry'] = schools['geometry'].to_crs(epsg=4326) grid = grid.loc[(grid['Population'] > 0)] grid['geoid'] = grid.index.astype(str) schools['geoid'] = schools.index.astype(str) grid = grid[['geoid', 'Population', 'geometry']] jsontxt = grid.to_json() # Calculate x and y coordinates of the line roads['points_list'] = roads.apply(getLinesAsPointList, geom='geometry', axis=1) map_osm.choropleth(geo_data=jsontxt, data=grid, columns=['geoid', 'Population'], key_on="feature.id",fill_color='YlOrRd', fill_opacity=0.5, line_opacity=0.2, line_color='white', line_weight=0,threshold_scale=[0, 5, 10, 100, 1000],legend_name='Population in Tartu', highlight=False, smooth_factor=1.0) # list of lat lon coordinate pair tuples roadpoints = list(roads['points_list']) .. ipython:: python :suppress: for road in roadpoints: folium.PolyLine(road, color="grey", weight=2.5, opacity=1).add_to(map_osm) .. ipython:: python :suppress: # Create Address points on top of the map for idx, row in schools.iterrows(): # Get lat and lon of points lon = row['geometry'].x lat = row['geometry'].y # Get address information names = row['name'] # Add marker to the map folium.RegularPolygonMarker(location=[lat, lon], popup=names, fill_color='#2b8cbe', number_of_sides=6, radius=8).add_to(marker_cluster) .. ipython:: python :suppress: outfp = os.path.join(os.path.abspath('data'), 'pop15.html') map_osm.save(outfp)