Stylish Maps

 

Intro

I’ve used OSMnx for some projects at work in the past, and it came to my attention that it could be used to create some nice-looking maps with some work. Browsing around, found one or two interesting-looking takes on it, so I decided to have a go at it.

CodeDev

Load libraries

Let’s start by importing and setting up our libraries. In particular, let’s turn the use_cache option to True to save time in downloading repeated data:

from os import path
import functions as fun
import matplotlib.pyplot as plt
ox.config(log_console=False, use_cache=True)

Setup style and coords

First, let’s select a cool location and title for our map. We do this by getting the latitude, longitude coordinates from Google Maps.

(lat, lon) = (43.77325343869306, 11.256711217026762)
dst = 10000
(label, fName) = (
    "Santa Maria dil Fiore\nFirenze, IT",
    "Firenze"
)


(bgColor, bdColor) = ('#100F0F22', '#ffffff11')
if TYPE=='Modern':
    (rdColor, rdAlpha, rdScale, txtColor) = ('#ffffff', .400, 4.75, '#ffffff')
else:
    (rdColor, rdAlpha, rdScale, txtColor) = ('#000000', .5, 5, '#100F0FDD')

Get roads and buildings

Now, for the fun part, let’s download the networks object using OSMnx:

print("* Processing {}".format(fName), end='\r')
G = ox.graph_from_point(
    point, dist=DST, network_type='all',
    retain_all=True, simplify=True, truncate_by_edge=True
)

and, if so desired, the building footprints (takes a while):

if BLDG:
    gdf = ox.geometries.geometries_from_point(
        point, tags={'building':True} , dist=DST
    )

Apply style to roads

With the data already downloaded, we can change the style of our streets by using some of the ideas from this post and changing the width to scale as a proportion of the road length:

data = [i[-1] for i in G.edges(keys=True, data=True)]
(roadColors, roadWidths) = ([], [])
for item in data:
    if "length" in item.keys():
        if item["length"] <= 100:
            linewidth = 0.15*rdScale
            color = fun.lighten(rdColor, .7)
        elif item["length"] > 100 and item["length"] <= 200:
            linewidth = 0.25*rdScale
            color = fun.lighten(rdColor, .775)
        elif item["length"] > 200 and item["length"] <= 400:
            linewidth = 0.3*rdScale
            color = fun.lighten(rdColor, .85)
        elif item["length"] > 400 and item["length"] <= 800:
            linewidth = 0.5*rdScale
            color = fun.lighten(rdColor, 0.9)
        else:
            linewidth = 0.6*rdScale
            color = fun.lighten(rdColor, 1.0)
    else:
        color = rdColor
        linewidth = 0.10
    roadColors.append(color)
    roadWidths.append(linewidth)

Generate map

For the plotting end of the script. We start by laying down our graph object in a standard matplotlib object, along with the buildings footprints:

(fig, ax) = ox.plot_graph(
    G, node_size=0,figsize=(40, 40), 
    dpi=DPI, bgcolor=bgColor,
    save=False, edge_color=roadColors, edge_alpha=rdAlpha,
    edge_linewidth=roadWidths, show=False
)
if BLDG:
    (fig, ax) = ox.plot_footprints(
        gdf, ax=ax,
        color=bdColor, dpi=DPI, save=False, show=False, close=False
    )

Now for a final couple of touches, we’re going to add a cross marker at the location, along with the title text, and the coordinates of the place:

if MARKER:
    ax.scatter(
        point[1], point[0], marker="x",
        zorder=10, color=txtColor,
        s=7500, linewidth=5
    )
ax.text(
    0.5, 0.825, '{}'.format(label), family=FONT_FACE,
    horizontalalignment='center', verticalalignment='center', 
    transform=ax.transAxes, color=txtColor, fontsize=FONT_SIZE
)
if COORDS:
    ax.text(
        0.5, 0.2, 'N: {}\nW: {}'.format(latStr, lonStr), family=FONT_FACE,
        horizontalalignment='center', verticalalignment='center', 
        transform=ax.transAxes, color=txtColor, fontsize=FONT_SIZE*0.8
    )

Export

Now, we save our map with transparent background:

fig.tight_layout(pad=0)
fig.savefig(
    path.join(PATH, fName+'.png'), 
    dpi=DPI, bbox_inches='tight', format="png", 
    facecolor=fig.get_facecolor(), transparent=True
)
plt.clf();plt.cla();plt.close(fig);plt.gcf();

Inkscape overlay

This final part is entirely optional, but I wanted to automate the whole process of overlaying the maps on textures without any further input on my end. Inkscape’s svg files are standard XML format, so I went ahead and, after setting up my canvas, I replaced the map’s filename with the string MAP_IMG so that I could replace it automatically from the main script. Additionally, inkscape can be launched from the terminal, so I went ahead and generated the command to launch it directly from my python script:

fin = open(path.join(PATH, 'PANEL.svg'), "rt")
data = fin.read()
data = data.replace('MAP_IMG', fName)
fin.close()
fin = open(path.join(PATH, 'PANEL.svg'), "wt")
fin.write(data)
fin.close()
# Export composite image ------------------------------------------------------
cmd = [
    'inkscape', 
    '--export-type=png', 
    '--export-dpi='+str(DPI), 
    '--export-area-page',
    path.join(PATH, 'PANEL.svg'), 
    '--export-filename='+path.join(PATH, 'MAP_'+fName+'.png')
]
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Return svg to original state ------------------------------------------------
fin = open(path.join(PATH, 'PANEL.svg'), "rt")
data = fin.read()
data = data.replace(fName,'MAP_IMG')
fin.close()
fin = open(path.join(PATH, 'PANEL.svg'), "wt")
fin.write(data)
fin.close()
print(" "*80, end='\r')

Gallery

Code Repo