Skip to main content

4. Tile Servers

map of the port of Hamburg with sea marks overlay and attribution

Tiles​

The map consists of square images, the tiles, which are loaded from a tile server. Every tile has a zoom level z and a position x and y, and the server provides it under a URL that contains these three values. With set_tile_server you can use any server that works like this. The URL contains the placeholders {z}, {x} and {y}, and max_zoom is the highest zoom level the server supports:

self.map_widget.set_tile_server("https://a.tile.opentopomap.org/{z}/{x}/{y}.png", max_zoom=17)

OpenStreetMap​

By default TkinterMapView loads its tiles from the servers of OpenStreetMap. These servers are run by volunteers and donations, and they may only be used under the conditions of the tile usage policy. The most important points:

  • Send a User-Agent that names your app. Since version 1.32, TkinterMapView sends the name of your script by default, and you can set your own name with the user_agent argument or with set_user_agent(). Requests with a generic User-Agent are blocked, and the map only shows "Access blocked" tiles.
  • Use the URL https://tile.openstreetmap.org/{z}/{x}/{y}.png. Other hostnames, like a.tile.openstreetmap.org, can be slower or removed without notice. Since version 1.32 this is also the default URL of TkinterMapView.
  • Show the attribution "© OpenStreetMap contributors" visibly on the map.
  • Don't download tiles in bulk or for offline use from these servers.

If your app is used a lot, use a tile provider that is made for this, often with an API key, or run your own tile server.

Attribution​

Most tile servers require that their source is visible on the map. A small label in the bottom right corner of the map is enough. The label is placed on the map widget with place, so it stays in the corner when the window is resized:

self.attribution_label = customtkinter.CTkLabel(self.map_widget, text="", height=20, corner_radius=0,
fg_color=("gray90", "gray20"),
font=customtkinter.CTkFont(size=11))
self.attribution_label.place(relx=1.0, rely=1.0, anchor="se")

Switch the tile server​

In this example the user can switch between OpenStreetMap and OpenTopoMap, a map with contour lines. The servers are stored in a dictionary with their URL, their maximum zoom level and their attribution:

TILE_SERVERS = {
"OpenStreetMap": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"max_zoom": 19,
"attribution": "© OpenStreetMap contributors",
},
"OpenTopoMap": {
"url": "https://a.tile.opentopomap.org/{z}/{x}/{y}.png",
"max_zoom": 17,
"attribution": "© OpenStreetMap contributors, SRTM | © OpenTopoMap (CC-BY-SA)",
},
}

When the server changes, the map gets the new URL and the attribution label the new text. If the map is zoomed in further than the new server supports, the zoom is reduced:

def change_tile_server(self, name):
server = self.tile_servers[name]
self.map_widget.set_tile_server(server["url"], max_zoom=server["max_zoom"])
if self.map_widget.zoom > server["max_zoom"]:
self.map_widget.set_zoom(server["max_zoom"])
self.attribution_label.configure(text=f" {server['attribution']} ")

Tile providers with an API key work the same way, the key is part of the URL. For example the cycle map of Thunderforest:

self.map_widget.set_tile_server("https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=YOUR_API_KEY")

The examples in the README of TkinterMapView also contain URLs of Google Maps. Google only allows its map tiles through its official APIs, so check the terms of Google before you use these URLs in an app.

Overlays​

An overlay is a second tile server, whose tiles are drawn on top of the map. They are transparent except for the things they show, like the sea marks of OpenSeaMap:

self.map_widget.set_overlay_tile_server("https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png")

set_overlay_tile_server(None) removes the overlay. Because the tiles are combined when they are loaded, the map has to load its tiles again after the overlay changes, which is done by calling set_tile_server again.

Full code​

tile_servers.py
import customtkinter
import tkintermapview

TILE_SERVERS = {
"OpenStreetMap": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"max_zoom": 19,
"attribution": "© OpenStreetMap contributors",
},
"OpenTopoMap": {
"url": "https://a.tile.opentopomap.org/{z}/{x}/{y}.png",
"max_zoom": 17,
"attribution": "© OpenStreetMap contributors, SRTM | © OpenTopoMap (CC-BY-SA)",
},
}
SEA_MARKS_OVERLAY = "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png"


class App(customtkinter.CTk):
def __init__(self):
super().__init__()

self.title("Tile servers")
self.geometry("800x560")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)

self.tile_servers = TILE_SERVERS

self.top_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.top_frame.grid(row=0, column=0, sticky="ew")
self.server_menu = customtkinter.CTkOptionMenu(self.top_frame, values=list(self.tile_servers),
command=self.change_tile_server)
self.server_menu.grid(row=0, column=0, padx=20, pady=10)
self.overlay_switch = customtkinter.CTkSwitch(self.top_frame, text="Sea marks", command=self.toggle_overlay)
self.overlay_switch.grid(row=0, column=1, padx=0, pady=10)

self.map_widget = tkintermapview.TkinterMapView(self, corner_radius=0)
self.map_widget.grid(row=1, column=0, sticky="nsew")

# the attribution of the tile server has to be visible on the map
self.attribution_label = customtkinter.CTkLabel(self.map_widget, text="", height=20, corner_radius=0,
fg_color=("gray90", "gray20"),
font=customtkinter.CTkFont(size=11))
self.attribution_label.place(relx=1.0, rely=1.0, anchor="se")

self.change_tile_server("OpenStreetMap")
self.map_widget.set_position(53.5450, 9.9660) # port of Hamburg
self.map_widget.set_zoom(14)

def change_tile_server(self, name):
server = self.tile_servers[name]
self.map_widget.set_tile_server(server["url"], max_zoom=server["max_zoom"])
if self.map_widget.zoom > server["max_zoom"]:
self.map_widget.set_zoom(server["max_zoom"])
self.attribution_label.configure(text=f" {server['attribution']} ")

def toggle_overlay(self):
if self.overlay_switch.get() == 1:
self.map_widget.set_overlay_tile_server(SEA_MARKS_OVERLAY)
else:
self.map_widget.set_overlay_tile_server(None)
# load the tiles again, so the change is visible
self.change_tile_server(self.server_menu.get())


app = App()
app.mainloop()

Common errors​

"Access blocked", error 403 or gray tiles

The tile server rejected the requests. Check that you use TkinterMapView 1.32 or newer, that the URL is correct and that you follow the usage policy of the server. For OpenStreetMap, use https://tile.openstreetmap.org/{z}/{x}/{y}.png. If many people use your app, switch to a tile provider with an API key.

All tiles are empty after setting an overlay

TkinterMapView versions before 1.32 don't work with overlays in Pillow 10 or newer. Update TkinterMapView with pip install --upgrade tkintermapview.

The map is empty at high zoom levels

The server doesn't provide tiles for this zoom level. Pass the correct max_zoom to set_tile_server.