Last active
January 22, 2023 21:46
-
-
Save jgibbard/02794f0d2f60b7f5e22dba8d16fba790 to your computer and use it in GitHub Desktop.
Generate a PNG map image from a shapefile
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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