Skip to content

Instantly share code, notes, and snippets.

@jgibbard
Last active January 22, 2023 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgibbard/02794f0d2f60b7f5e22dba8d16fba790 to your computer and use it in GitHub Desktop.
Save jgibbard/02794f0d2f60b7f5e22dba8d16fba790 to your computer and use it in GitHub Desktop.
Generate a PNG map image from a shapefile
#!/usr/bin/env python3
import math
import shapefile
from PIL import Image, ImageDraw
# Longitude range to display in degrees
x_range = [-180,180]
# Latitude range to display in degrees
y_range = [-90,90]
# Minimum size of the larger dimension of the image
min_size_px = 1500
# wget https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_0_countries_lakes.zip
# unzip ne_50m_admin_0_countries_lakes.zip
# rm ne_50m_admin_0_countries_lakes.zip *.cpg *.dbf *.prj *.README.html *.VERSION.txt *.shx
sf = shapefile.Reader("ne_50m_admin_0_countries_lakes.shp")
shapes = sf.shapes()
x_range = [math.floor(x_range[0]), math.ceil(x_range[1])]
y_range = [math.floor(y_range[0]), math.ceil(y_range[1])]
x_diff = x_range[1] - x_range[0]
y_diff = y_range[1] - y_range[0]
if x_diff >= y_diff:
pixels_per_degree = math.ceil(min_size_px / x_diff)
else:
pixels_per_degree = math.ceil(min_size_px / y_diff)
# Oversize and then resize at the end to allow
# to apply anti-aliasing filter
oversample = 4
x_size = x_diff * pixels_per_degree * oversample
y_size = y_diff * pixels_per_degree * oversample
img = Image.new("RGB", [x_size, y_size], "#8ab4f8")
canvas = ImageDraw.Draw(img)
# Mercator projection (linear mapping from degrees to pixels)
scale = x_size / x_diff
offset_x = -(x_range[0] * pixels_per_degree * oversample)
offset_y = -(y_range[0] * pixels_per_degree * oversample)
for country in shapes:
# Countries can be multipart polygons
# So get list of the start and end index values for each part
parts = list(country.parts)
# Last index isn't in the parts list, so add it
parts.append(len(country.points))
# Step through each pair of index value and draw the polygon
for lower, upper in zip(parts, parts[1:]):
# Apply scaling. X increases left to right, Y increased top to bottom
country_scaled = [((point[0]*scale)+offset_x, y_size-(offset_y+(scale*point[1])))
for point in country.points[lower:upper]]
canvas.polygon(country_scaled, fill ="#a8dab5", outline ="black")
img = img.resize((x_size//oversample, y_size//oversample), Image.LANCZOS)
img.save("map.png")
img.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment