<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="favicon.png">
<title></title>
<meta name="description" content="" />
<link href="style.css" rel="stylesheet" type="text/css" />
<style>
</style>
</head>
<body>
<div id="container">
<form action="create" method="post">
Regions: <input type="text" value="SC, NC" name="regions" />
<div>
Name: <input type="text" value="" name="name" />
</div>
<button type="submit">Submit</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="favicon.png">
<title></title>
<meta name="description" content="" />
<link href="style.css" rel="stylesheet" type="text/css" />
<style>
</style>
</head>
<body>
<div id="container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox='0 0 131.21 100' width='524.84px' height='400px'>
<g id="region-GA" fill='#428'>
<path id="GA-0" d="M 10.45 87.96 9.81 86.18 8.99 84.25 9.35 81.77 9.67 79.1 8.97 76.24 9.69 71.4 11.82 68.72 10.69 65.99 8.84 61.2 5.8 45.86 2.44 26.44 12.39 26.58 19.28 26.42 35.35 26.55 33.93 27.86 31.86 30.84 35.35 33.44 37.59 34.4 39.99 39.34 41.53 42.11 46.03 45.79 46.91 47.72 49.98 50.25 51.48 53.93 55.61 57.03 56.53 60.55 57.29 62.25 56.85 63.37 59.25 65.05 60.52 67.92 60.5 70.82 61.69 71.39 63.9 72.17 57.81 81.17 55.82 91.88 53.1 91.6 50.54 90.5 48.99 91.01 48.95 96.22 47.28 97.56 46.33 94.94 25.39 92.7 12.41 92.01 10.45 87.96" stroke="black" />
</g>
<g id="region-SC" fill='#428'>
<path id="SC-0" d="M 63.9 72.17 61.69 71.39 60.5 70.82 60.52 67.92 59.25 65.05 56.85 63.37 57.29 62.25 56.53 60.55 55.61 57.03 51.48 53.93 49.98 50.25 46.91 47.72 46.03 45.79 41.53 42.11 39.99 39.34 37.59 34.4 35.35 33.44 31.86 30.84 33.93 27.86 35.35 26.55 36.64 26.08 43.61 23.38 55.52 23.51 61.56 24.24 61.66 25.63 62.96 24.59 64.98 27.26 64.95 29.09 79.29 29.25 93.73 44.04 87.19 49.74 85.35 54.93 71.18 64.9 63.9 72.17" stroke="black" />
</g>
<g id="region-NC" fill='#492'>
<path id="NC-0" d="M 35.35 26.55 19.28 26.42 19.57 23.08 22.3 22.09 23.2 20.39 25.03 18.47 27.64 18.05 30.68 17.32 33.63 15.95 34.9 14.54 37.36 13.27 37.3 12.12 40.54 9.96 41.59 11.36 46.35 8.35 48.58 8.67 50.57 5.98 53.2 5.29 53.06 3.0 53.38 0.98 48.34 1.22 53.38 0.98 75.16 1.68 100.89 1.75 114.56 1.62 126.79 1.54 128.41 1.53 130.23 17.51 122.02 29.23 108.66 33.89 100.18 43.04 93.73 44.04 79.29 29.25 64.95 29.09 64.98 27.26 62.96 24.59 61.66 25.63 61.56 24.24 55.52 23.51 43.61 23.38 36.64 26.08 35.35 26.55" stroke="black" />
</g>
</svg>
</div>
</body>
</html>
import random
from pyproj import Transformer
from shapely.geometry import Polygon, Point, MultiPolygon
import shapefile
from flask import Flask, render_template, request, send_from_directory, redirect
app = Flask(__name__)
def sortSecond(a):
return a[1]
@app.route('/', methods = ['GET'])
def index():
if request.method == 'GET':
return render_template('create.html')
@app.route('/create', methods = ['GET', 'POST'])
def create():
if request.method == 'GET':
return render_template('create.html')
else:
filename = "ne_110m_admin_1_states_provinces_lakes"
nameStr = "name"
abbStr = "postal"
outname = request.form.get('name')
if not outname or len(outname) == 0:
outname = "unnamed-region"
print(outname)
regionsPost = request.form.get('regions').split(",")
regions = []
for i in range(0,len(regionsPost)):
regions.append(regionsPost[i].strip().upper())
print(regions)
cFile = filename+"/"+filename+".shp"
regionShape = shapefile.Reader(cFile)
fields = regionShape.fields[1:]
idx = 0
for field in fields:
if field[0] == nameStr:
idx3 = idx
if field[0] == abbStr:
idx4 = idx
idx+=1
regionPolygons = regionShape.shapeRecords()
regionData = {"regions":{},"width":100,"viewBox":"0 0 100 100"}
cidx = 0
transformer = Transformer.from_crs("epsg:4326", "epsg:3857")
bounds = [-1000,-1000,-1000,-1000]
scale = 1
width = 100
for region in regionPolygons:
geo = region.shape.__geo_interface__
geoType = geo["type"]
props = region.record
regionID = props[idx4].strip().upper()
if regionID not in regions:
continue
paths = []
if geoType == 'Polygon':
coords = geo['coordinates'][0]
path = []
for ii in range(0,len(coords)):
out = transformer.transform(coords[ii][1],coords[ii][0])
out = [out[0],-1*out[1]]
path.append(out)
if bounds[0]==-1000 or out[0]<bounds[0]:
bounds[0] = out[0]
if bounds[1]==-1000 or out[0]>bounds[1]:
bounds[1] = out[0]
if bounds[2]==-1000 or out[1]<bounds[2]:
bounds[2] = out[1]
if bounds[3]==-1000 or out[1]>bounds[3]:
bounds[3] = out[1]
poly = Polygon(coords)
paths.append([path,0])
elif geoType == 'MultiPolygon':
allPolys = []
for i in range(0,len(geo['coordinates'])):
coords = geo['coordinates'][i][0]
path = []
for ii in range(0,len(coords)):
out = transformer.transform(coords[ii][1],coords[ii][0])
out = [out[0],-1*out[1]]
path.append(out)
if bounds[0]==-1000 or out[0]<bounds[0]:
bounds[0] = out[0]
if bounds[1]==-1000 or out[0]>bounds[1]:
bounds[1] = out[0]
if bounds[2]==-1000 or out[1]<bounds[2]:
bounds[2] = out[1]
if bounds[3]==-1000 or out[1]>bounds[3]:
bounds[3] = out[1]
poly = Polygon(coords)
allPolys.append(poly)
paths.append([path,poly.area])
poly = MultiPolygon(allPolys)
else:
print(geoType)
scale = 100/(bounds[3]-bounds[2])
bounds[0] -= 1/scale
bounds[1] += 1/scale
bounds[2] -= 1/scale
bounds[3] += 1/scale
scale = 100/(bounds[3]-bounds[2])
paths.sort(key=sortSecond,reverse=True)
regionName = props[idx3]
width = round((bounds[1]-bounds[0])*scale,2)
color = "#"+str(random.randint(0,9))+str(random.randint(0,9))+str(random.randint(0,9))
regionData["regions"][regionID]={"name":regionName,"id":regionID,"paths":paths,"fillColor":color}
regionData["width"]=width
regionData["viewBox"]="0 0 "+str(width)+" 100"
for r in regionData["regions"]:
pathsRaw = regionData["regions"][r]["paths"]
regionData["regions"][r]["paths"] = []
for i in range(0,len(pathsRaw)):
path = "M"
lastCoord = [-10,-10]
firstCoord = [-10,-10]
for ii in range(0,len(pathsRaw[i][0])):
x = round((pathsRaw[i][0][ii][0]-bounds[0])*scale,2)
y = round((pathsRaw[i][0][ii][1]-bounds[2])*scale,2)
if ii == 0:
firstCoord = [x,y]
if lastCoord[1]==y and lastCoord[0]==x:
continue
lastCoord = [x,y]
path += " "+str(x)+" "+str(y)
if lastCoord[0] != firstCoord[0] or lastCoord[1] != firstCoord[1]:
path += " "+str(firstCoord[0])+" "+str(firstCoord[1])
regionData["regions"][r]["paths"].append({"id":r+"-"+str(i),"d":path})
output = render_template("template.html",regions=regionData["regions"],viewBox=regionData["viewBox"],width=regionData["width"])
with open("svgs/"+outname+".html", "w") as fh:
fh.write(output)
return redirect("svgs/"+outname+".html", code=302)
@app.route('/svgs/<path:path>')
def send_svg(path):
return send_from_directory('svgs',path)
app.run(host='0.0.0.0', port=81)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="favicon.png">
<title></title>
<meta name="description" content="" />
<link href="style.css" rel="stylesheet" type="text/css" />
<style>
</style>
</head>
<body>
<div id="container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox='{{viewBox}}' width='{{width * 4}}px' height='400px'>
{% for region in regions %}
<g id="region-{{region}}" fill='{{regions[region]["fillColor"]}}'>
{% for path in regions[region]["paths"] %}
<path id="{{path.id}}" d="{{path.d}}" stroke="black" />
{% endfor %}
</g>
{% endfor %}
</svg>
</div>
</body>
</html>
About
This repl creates an html files with SVG shapes for a collection of U.S. states using Python.
To view this on TripleLog, click here.
Setup
The packages should automatically install if using Replit. You need pyShp (import shapefile), shapely, pyproj, and flask. Install (with dependencies) any that need installing. You can also use fiona or other geo packages, but installing GDAL on Replit seems to not always work.
Running the main.py file should start a flask server that will be on the localhost or your repl.co domain. Then visit {{baseurl}} (either something.repl.co or localhost:81) or {{baseurl}}/create to choose which states to add to your SVG. This page will then redirect you to your created html file.
Python
Use the jinja2 templating to generate the files from templates/template.html. The syntax is similar to nunjucks.
We need to read the shapefile and grab the coordinates for whichever states we want to display.
You can use a different shapefile or geoJSON object, but you will need to adjust some of the early code to make everything work.
HTML
A template file called template.html is used to eventually create the HTML file with the final SVG shapes. The flask server then serves these files automatically once they are generated to the svgs directory.