Title: Make a close-up map in Python
This example displays a map of the United States at a relatively small scale. When you move the mouse over the map, the program displays a magnified version of whatever is below the mouse. It's a pretty cool effect!
Loading Images
When the program starts, the main app class executes the following constructor to get things ready to roll.
class close-upMapApp:
def __init__(self):
self.window = tk.Tk()
self.window.title('closeup_map')
self.window.protocol('WM_DELETE_WINDOW', self.kill_callback)
# Make the canvas.
self.canvas = tk.Canvas(self.window, bg='white',
borderwidth=0, highlightthickness=0, cursor='crosshair')
self.canvas.pack(padx=10, pady=10,
side=tk.TOP, fill=tk.BOTH, expand=True)
# Load the full-scale map.
map_name = 'usmap.gif'
self.map_image = Image.open(map_name).convert('RGBA')
# Load the small-scale map.
# (I tried resizing the large map, but a lof of state borders
# and other thin features came out looking dashed.)
small_map_name = 'usmapsmall.jpg'
self.small_map_image = Image.open(small_map_name).convert('RGBA')
# Display the small-scale map.
self.small_map_photo_image = ImageTk.PhotoImage(self.small_map_image)
self.canvas.create_image(0, 0, image=self.small_map_photo_image,
anchor='nw')
wid = self.small_map_image.width
hgt = self.small_map_image.height
self.canvas.config(width=wid, height=hgt)
# Radius of the close-up area in small and large scales.
self.radius = 50
self.scale = self.map_image.width / self.small_map_image.width
# Track mouse motion over the canvas.
self.canvas.bind('', self.mouse_move)
# Display the window.
self.window.focus_force()
self.window.mainloop()
This code first initializes tkinter and creates a Canvas widget. It then loads a full-scale image of the map, converts it to the RGBA format, and saves the image in self.map_image.
Next, the code repeats those steps to load the smaller-scale image shown in the picture. As the comment mentions, I tried just loading the large scale image and resizing it to build the smaller-scale image. Unfortunately, aliasing effects broke up the linear features on the map making things like state borders look like dashed lines. I got a much better result by using MS Paint to resize the full-scale image and save it in a separate file.
Having loaded the large- and small-scale maps, the program displays the small map in the Canvas and sizes that widget to fit the image. It sets the radius of the close-up area, calculates the big-to-small map scale, and displays the window.
The more interesting work happens when the mouse moves over the Canvas widget.
Mouse Movement
When the mouse moves over the Canvas widget, the following event handler executes.
def mouse_move(self, event):
'''Display the close-up.'''
self.show_closeup(event.x, event.y)
This event handler simply calls the following show_closeup method.
def show_closeup(self, x, y):
'''Display the close-up centered at (x, y).'''
# Make a mask image.
wid = self.map_image.width
hgt = self.map_image.height
mask = Image.new('L', (wid, hgt), 0)
# Figure out where to draw the big map to position the close-up.
big_x = self.scale * x
big_y = self.scale * y
# Make a white circle for the close-up.
dr = ImageDraw.Draw(mask)
bounds = (big_x - self.radius, big_y - self.radius,
big_x + self.radius, big_y + self.radius)
dr.ellipse(bounds, fill='white', outline='white')
# mask.save('mask.png')
# Copy the small map image so we don't mess up the original.
map_copy = self.small_map_image.copy()
# Draw the big map onto the copy of the little one using the mask.
paste_x = int(x - big_x)
paste_y = int(y - big_y)
map_copy.paste(self.map_image, box=(paste_x, paste_y), mask=mask)
# Draw a circle around the area.
dr = ImageDraw.Draw(map_copy)
rect = (x-self.radius, y-self.radius, x+self.radius, y+self.radius)
dr.ellipse(rect, outline='blue', width=3)
# map_copy.save('copy.png')
# Display the result.
self.small_map_photo_image = ImageTk.PhotoImage(map_copy)
self.canvas.delete(tk.ALL)
self.canvas.create_image(0, 0, image=self.small_map_photo_image,
anchor='nw')
The show_closeup method displays the close-up under the mouse. It starts by building a mask image that will let it draw a circular area under the mouse. It makes the mask the same size as the full-scale map image. The 'L' parameter in the Image.new call makes this a grayscale image. The final parameter, 0, makes the mask initially filled with color 0, which is black. (The red, green, and blue components all have value 0.)
Next, the code multiplies the mouse's current X and Y coordinates by the saved scale factor self.scale. That gives the location of the mouse on the larger, hidden map image.
To make the mask work properly, the program needs to draw a white circle at the location on the mask where the large-scale image should show through. To do that, it needs to find the bounding coordinates for the circle. Those coordinates are the mouse coordinates scaled onto the full-scale map, plus or minus the radius.
After finding the circle's bounds, the code draws an ellipse (circle) at that position, filled and outlined in white. (Commented out code saves the mask image so you can look at it if you like.)
Now the program makes a copy of the small-scale map so it can draw the close-up area on it without messing up the original image. (We'll need it again later every time the mouse moves.)
The trickiest part of this code is bit that figures out where to position the large-scale image so the mask's white circle is centered at the mouse's current position. To do that, the code starts at the mouse's position (x, y) and subtracts the mouse's position on the large-scale map (big_x, big_y). The result gives the coordinates of the large-scale map's upper left corner.
The method then pastes the large-scale map at that position using the mask image as a mask. Only the pixels in the full-scale image that correspond to the mask's white circle are drawn onto the copy of the small-scale map.
At this point, the copy shows the small-scale map with the close-up area pasted in. The result looks better, though, if we draw a circle around that area, so that's what the code does next. It creates an ImageDraw.Draw object for the map copy and uses its ellipse method to draw a blue circle. (Commented out code saves the image so you can see it.)
Finally, the method converts the map copy into a PhotoImage that tkinter can understand and displays it on the Canvas widget.
Conclusion
This example creates an interesting type of map that lets you show an overview with a close-up of an area of interest. It may not be the most necessary application, but it's pretty interesting and even kind of fun to use.
In my next post, I'll show how you can adapt this example to show only part of an image while leaving the rest dark, sort of as if you're looking at the image with a flashlight. Meanwhile, download this example to experiment with it and to see additional details.
|