Skip to content

Instantly share code, notes, and snippets.

@dtlanghoff
Last active June 6, 2023 08:07
Show Gist options
  • Save dtlanghoff/551f79ca5df145051a47e10bed55ff16 to your computer and use it in GitHub Desktop.
Save dtlanghoff/551f79ca5df145051a47e10bed55ff16 to your computer and use it in GitHub Desktop.
Slektskart
import io
import locale
import math
import lark
import pyproj
from PIL import Image, ImageDraw
from flask import Flask, make_response, render_template
from flask_talisman import Talisman
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField, TextAreaField
from wtforms.validators import InputRequired, Length
locale.setlocale(locale.LC_COLLATE, 'nn_NO.UTF-8')
app = Flask(__name__)
app.secret_key = ...
csp = {'default-src': ["'self'"],
'script-src': ['cdn.jsdelivr.net',
'code.jquery.com'],
'style-src': ['cdn.jsdelivr.net',
"'unsafe-inline'"]}
talisman = Talisman(app, content_security_policy=csp)
grammar = r"""ancestry: "(" SIGNED_NUMBER ","? SIGNED_NUMBER ancestry* ")"
%ignore WS
%ignore SH_COMMENT
%import common.WS
%import common.SH_COMMENT
%import common.SIGNED_NUMBER"""
class Transformer(lark.Transformer):
def ancestry(self, s):
return [(float(s[0].value), float(s[1].value)), *s[2:]]
parser = lark.Lark(grammar, start='ancestry')
transformer = Transformer()
def projection(name, size, **kwargs):
def transform(latitude, longitude, extent):
width, height = size
north, south, west, east = extent
x = width * (longitude - west) / (east - west)
y = height * (north - latitude) / (north - south)
return x, y
def equirectangular():
return lambda lat, lon: transform(lat, lon, kwargs['extent'])
def winkel_tripel():
φ_1 = math.radians(kwargs['φ_1'])
λ_0 = math.radians(kwargs['λ_0'])
def sinc(x):
return 1.0 if x == 0.0 else math.sin(x) / x
def to_yx(latitude, longitude):
φ, λ = math.radians(latitude), math.radians(longitude)
λ = ((λ - λ_0 + math.pi) % (2*math.pi)) - math.pi
α = math.acos(math.cos(φ) * math.cos(λ / 2))
x = (λ * math.cos(φ_1) + (2 * math.cos(φ) * math.sin(λ / 2)) / sinc(α)) / 2
y = (φ + math.sin(φ) / sinc(α)) / 2
return y, x
extent = (math.pi/2,
-math.pi/2,
-math.pi/2 * (1 + math.cos(φ_1)),
math.pi/2 * (1 + math.cos(φ_1)))
return lambda lat, lon: transform(*to_yx(lat, lon), extent)
def equidistant_conic():
φ_1, φ_2 = math.radians(kwargs['φ_1']), math.radians(kwargs['φ_2'])
λ_0 = math.radians(kwargs['λ_0'])
n = (math.cos(φ_1) - math.cos(φ_2)) / (φ_2 - φ_1)
G = (math.cos(φ_1) / n) + φ_1
def to_yx(latitude, longitude):
φ, λ = math.radians(latitude), math.radians(longitude)
ρ = G - φ
θ = n * (λ - λ_0)
return G - ρ*math.cos(θ), ρ * math.sin(θ)
top = to_yx(kwargs['φ_top'], kwargs['λ_0'])[0]
bottom = to_yx(kwargs['φ_bot'], kwargs['λ_0'])[0]
extent = (top, bottom, (bottom-top)/2*size[0]/size[1], (top-bottom)/2*size[0]/size[1])
return lambda lat, lon: transform(*to_yx(lat, lon), extent)
def epsg():
t = pyproj.Transformer.from_crs('epsg:4326', f'epsg:{kwargs["epsg"]}')
return lambda lat, lon: transform(*t.transform(lat, lon), kwargs['extent'])
return {'equirectangular': equirectangular,
'equidistant-conic': equidistant_conic,
'winkel-tripel': winkel_tripel,
'epsg': epsg}[name]()
textarea_default_text = """(59.9 10.7 # eg
(60.4 5.3 # far i Bergen
(59.0 5.7) (59.4 5.3)) # besteforeldre
(63.4 10.4) # mor i Trondheim
)"""
maps = {'denmark': {'name': 'Danmark (ekvirektangulær)',
'filename': 'Denmark_location_map.svg.png',
'projection': projection('equirectangular', (2548, 2048), extent=(57.9, 54.3, 7.8, 15.4))},
'europe': {'name': 'Europa (Lambert asimutal likeareal)',
'filename': 'Europe_laea_location_map.svg.png',
'projection': projection('epsg', (2395, 2048), epsg=3035, extent=(5500000, 1350000, 2555000, 7405000))},
'norway': {'name': 'Noreg (ekvirektangulær)',
'filename': 'Norway_location_map.svg.png',
'projection': projection('equirectangular', (2215, 2353), extent=(71.5, 57.6, 4.1, 31.6))},
'norway-conic': {'name': 'Noreg (ekvidistant kjegle)',
'filename': 'Norway_adm_location_map.svg.png',
'projection': projection('equidistant-conic', (1857, 2048),
φ_top=71.7, φ_bot=58,
φ_1=67, φ_2=62, λ_0=16)},
'norway-innlandet': {'name': 'Innlandet, Noreg (ekvidistant kjegle)',
'filename': 'Norway_Innlandet_adm_location_map.svg.png',
'projection': projection('equidistant-conic', (1957, 2048),
φ_top=62.75, φ_bot=59.8,
φ_1=61.8, φ_2=60.8, λ_0=10.25)},
'norway-nordland': {'name': 'Nordland, Noreg (ekvirektangulær)',
'filename': 'Norway_Nordland_adm_location_map.svg.png',
'projection': projection('equirectangular', (1484, 2048), extent=(69.4, 64.8, 10.0, 18.5))},
'norway-troms': {'name': 'Troms, Noreg (ekvirektangulær)',
'filename': 'Norway_Troms_location_map.svg.png',
'projection': projection('equirectangular', (2491, 2048), extent=(70.5, 68.3, 15.4, 23.0))},
'norway-troms-finnmark': {'name': 'Troms og Finnmark, Noreg (ekvidistant kjegle)',
'filename': 'Norway_Troms_og_Finnmark_adm_location_map.svg.png',
'projection': projection('equidistant-conic', (2560, 1408),
φ_top=71.3, φ_bot=68.15,
φ_1=70.25, φ_2=69.25, λ_0=23.25)},
'scandinavia': {'name': 'Skandinavia (ekvirektangulær)',
'filename': 'Scandinavia_location_map.svg.png',
'projection': projection('equirectangular', (1630, 2048), extent=(71.5, 53.6, 3.8, 32.3))},
'world-w3': {'name': 'verda (Winkel III)',
'filename': 'World_location_map_(W3).svg.png',
'projection': projection('winkel-tripel', (3188, 1948), φ_1=math.degrees(math.acos(2/math.pi)), λ_0=11.5)}}
class Form(FlaskForm):
map_selection = SelectField('Kartutsnitt', default='norway',
choices=sorted([(key, info['name']) for key, info in maps.items()],
key=lambda i: locale.strxfrm(i[1])))
ancestry = TextAreaField('Slektsvandring', default=textarea_default_text, validators=[InputRequired(), Length(max=5000)])
submit = SubmitField('Teikn kart')
def color(generation):
category10 = '1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf'
generation = generation % 10
return f'#{category10[6*generation:6*(generation+1)]}'
def draw_ancestry(draw, ancestry, project, generation=0):
x, y = project(*ancestry[0])
for parent in ancestry[1:]:
draw.line([(x, y), project(*parent[0])], width=4, fill='black')
draw_ancestry(draw, parent, project, generation + 1)
draw.ellipse((x - 12, y - 12, x + 12, y + 12),
fill=color(generation), outline='black', width=4)
@app.route('/', methods=['GET', 'POST'])
def index():
form = Form()
if form.validate_on_submit():
selected_map = maps[form.map_selection.data]
try:
ancestry = transformer.transform(parser.parse(form.ancestry.data))
except lark.LarkError as e:
response = make_response(f'{type(e).__name__}: {e}')
response.mimetype = 'text/plain'
return response
image = Image.open(f'static/{selected_map["filename"]}')
draw = ImageDraw.Draw(image)
draw_ancestry(draw, ancestry, selected_map['projection'])
f = io.BytesIO()
image.save(f, 'png')
response = make_response(f.getvalue())
response.mimetype = 'image/png'
return response
return render_template('index.html', form=form)
<!doctype html>
<html lang="nn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Slektskart</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap-select.min.css" integrity="sha384-2SvkxRa9G/GlZMyFexHk+WN9p0n2T+r38dvBmw5l2/J3gjUcxs9R1GwKs0seeSh3" crossorigin="anonymous">
</head>
<body>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap-select.min.js" integrity="sha384-SfMwgGnc3UiUUZF50PsPetXLqH2HSl/FmkMW/Ja3N2WaJ/fHLbCHPUsXzzrM6aet" crossorigin="anonymous"></script>
<main role="main" class="container p-3">
{% block content %}
{% endblock %}
</main>
</body>
</html>
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-10">
<h1>Slektskart</h1>
<form class="mt-3" method="post">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.map_selection.label }}
{{ form.map_selection(class_='form-control selectpicker', data_live_search='true') }}
</div>
<div class="form-group">
{{ form.ancestry.label }}
{{ form.ancestry(class_='form-control text-monospace', rows=10) }}
</div>
{{ form.submit(class_='btn btn-primary') }}
</form>
</div>
</div>
{% endblock %}
click==8.1.3
Flask==2.1.2
flask-talisman==1.0.0
flask-wtf==1.0.1
gunicorn==20.1.0
itsdangerous==2.1.2
Jinja2==3.1.2
lark==1.1.2
MarkupSafe==2.1.1
Pillow==9.2.0
pyproj==3.3.1
Werkzeug==2.1.2
WTForms==3.0.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment