Simplify and improve text layouting. Still not optimal, but MUCH better. Also add a CLI flag for renaming cities
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
54021f5f68
commit
eedb3fde4d
4 changed files with 206 additions and 79 deletions
|
@ -5,4 +5,7 @@ pipeline:
|
||||||
image: python:3.9
|
image: python:3.9
|
||||||
commands:
|
commands:
|
||||||
- pip3 install -r requirements.txt
|
- pip3 install -r requirements.txt
|
||||||
- ./generate_map.py --cache-directory cache.example
|
- >-
|
||||||
|
./generate_map.py
|
||||||
|
--cache-directory cache.example
|
||||||
|
--rename 'Frankfurt am Main' 'Frankfurt'
|
||||||
|
|
278
generate_map.py
278
generate_map.py
|
@ -10,6 +10,7 @@ import base64
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
|
import random
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import pyproj
|
import pyproj
|
||||||
|
@ -240,11 +241,14 @@ def compute_bbox(ns):
|
||||||
|
|
||||||
class BoundingBox:
|
class BoundingBox:
|
||||||
|
|
||||||
def __init__(self, left, top, right, bottom):
|
def __init__(self, left, top, right, bottom, meta=None, base_weight=0):
|
||||||
self.left = left
|
self.left = left
|
||||||
self.top = top
|
self.top = top
|
||||||
self.right = right
|
self.right = right
|
||||||
self.bottom = bottom
|
self.bottom = bottom
|
||||||
|
self.meta = meta
|
||||||
|
self.base_weight = base_weight
|
||||||
|
self.finished = False
|
||||||
|
|
||||||
def __contains__(self, other):
|
def __contains__(self, other):
|
||||||
if isinstance(other, BoundingBox):
|
if isinstance(other, BoundingBox):
|
||||||
|
@ -263,6 +267,35 @@ class BoundingBox:
|
||||||
return True
|
return True
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if not isinstance(other, tuple) and not isinstance(other, list):
|
||||||
|
raise TypeError()
|
||||||
|
return BoundingBox(self.left + other[0], self.top + other[1], self.right + other[0], self.bottom + other[1], self.meta, self.base_weight)
|
||||||
|
|
||||||
|
def __iadd__(self, other):
|
||||||
|
if not isinstance(other, tuple) and not isinstance(other, list):
|
||||||
|
raise TypeError()
|
||||||
|
self.left += other[0]
|
||||||
|
self.top += other[1]
|
||||||
|
self.right += other[0]
|
||||||
|
self.bottom += other[1]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
if not isinstance(other, tuple) and not isinstance(other, list):
|
||||||
|
raise TypeError()
|
||||||
|
return BoundingBox(self.left - other[0], self.top - other[1], self.right - other[0], self.bottom - other[1], self.meta, self.base_weight)
|
||||||
|
|
||||||
|
def __isub__(self, other):
|
||||||
|
if not isinstance(other, tuple) and not isinstance(other, list):
|
||||||
|
raise TypeError()
|
||||||
|
self.left -= other[0]
|
||||||
|
self.top -= other[1]
|
||||||
|
self.right -= other[0]
|
||||||
|
self.bottom -= other[1]
|
||||||
|
self._weight = None
|
||||||
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self):
|
||||||
return self.right - self.left
|
return self.right - self.left
|
||||||
|
@ -271,90 +304,171 @@ class BoundingBox:
|
||||||
def height(self):
|
def height(self):
|
||||||
return self.bottom - self.top
|
return self.bottom - self.top
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
return (self.left + self.width/2, self.top + self.height/2)
|
||||||
|
|
||||||
|
def distance2(self, other):
|
||||||
|
c1 = self.center
|
||||||
|
c2 = other.center
|
||||||
|
return (c1[0] - c2[0])**2 + (c1[1] - c2[1])**2
|
||||||
|
|
||||||
def with_margin(self, margin):
|
def with_margin(self, margin):
|
||||||
return BoundingBox(self.left - margin, self.top - margin, self.right + margin, self.bottom + margin)
|
return BoundingBox(self.left - margin, self.top - margin, self.right + margin, self.bottom + margin, self.meta, self.base_weight)
|
||||||
|
|
||||||
|
def compute_weight(self, other, erfas, chaostreffs, ns):
|
||||||
|
w = 0
|
||||||
|
swm = self.with_margin(ns.font_distance)
|
||||||
|
swe = self.with_margin(ns.dotsize_erfa)
|
||||||
|
swc = self.with_margin(ns.dotsize_treff)
|
||||||
|
swme = swm.with_margin(ns.dotsize_erfa)
|
||||||
|
swmc = swm.with_margin(ns.dotsize_treff)
|
||||||
|
for o in other:
|
||||||
|
if o.meta['city'] == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if o in self:
|
||||||
|
if o.finished:
|
||||||
|
w += 50
|
||||||
|
else:
|
||||||
|
w += 3
|
||||||
|
if o in swm:
|
||||||
|
if o.finished:
|
||||||
|
w += 3
|
||||||
|
else:
|
||||||
|
w += 1
|
||||||
|
for city, location in erfas.items():
|
||||||
|
if city == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if location in swe:
|
||||||
|
w += 15
|
||||||
|
if location in swme:
|
||||||
|
w += 2
|
||||||
|
for city, location in chaostreffs.items():
|
||||||
|
if city == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if location in swc:
|
||||||
|
w += 5
|
||||||
|
if location in swmc:
|
||||||
|
w += 1
|
||||||
|
self._weight = w
|
||||||
|
|
||||||
|
def compute_finalized_weight(self, other, erfas, chaostreffs, ns):
|
||||||
|
w = 0
|
||||||
|
swm = self.with_margin(ns.font_distance)
|
||||||
|
swe = self.with_margin(ns.dotsize_erfa)
|
||||||
|
swc = self.with_margin(ns.dotsize_treff)
|
||||||
|
swme = swm.with_margin(ns.dotsize_erfa)
|
||||||
|
swmc = swm.with_margin(ns.dotsize_treff)
|
||||||
|
for o in other:
|
||||||
|
if o.meta['city'] == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if o in self:
|
||||||
|
w += 100
|
||||||
|
if o in swm:
|
||||||
|
w += 15
|
||||||
|
for city, location in erfas.items():
|
||||||
|
if city == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if location in swe:
|
||||||
|
w += 100
|
||||||
|
if location in swme:
|
||||||
|
w += 10
|
||||||
|
for city, location in chaostreffs.items():
|
||||||
|
if city == self.meta['city']:
|
||||||
|
continue
|
||||||
|
if location in swc:
|
||||||
|
w += 100
|
||||||
|
if location in swmc:
|
||||||
|
w += 5
|
||||||
|
self._weight = w
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weight(self):
|
||||||
|
return self._weight + self.base_weight
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_optimal(self):
|
||||||
|
return self._weight == 0
|
||||||
|
|
||||||
|
|
||||||
def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
|
def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
|
||||||
font = ImageFont.truetype(ns.font, ns.font_size)
|
font = ImageFont.truetype(ns.font, ns.font_size)
|
||||||
pil = ImageDraw(Image.new('P', size))
|
pil = ImageDraw(Image.new('P', size))
|
||||||
|
|
||||||
lboxes = {}
|
candidates = {}
|
||||||
rboxes = {}
|
|
||||||
for city, location in erfas.items():
|
for city, location in erfas.items():
|
||||||
lbox = pil.textbbox((location[0] + ns.font_distance, location[1] - ns.font_size/2), city, font=font, anchor='lt')
|
text = city
|
||||||
lbox = BoundingBox(*lbox)
|
for rfrom, to in ns.rename:
|
||||||
rbox = pil.textbbox((location[0] - ns.font_distance, location[1] - ns.font_size/2), city, font=font, anchor='rt')
|
if rfrom == city:
|
||||||
rbox = BoundingBox(*rbox)
|
text = to
|
||||||
lboxes[city] = lbox
|
break
|
||||||
rboxes[city] = rbox
|
rbox = pil.textbbox((location[0] + ns.font_distance, location[1] - ns.font_size/2), text, font=font, anchor='lt')
|
||||||
|
rbox = BoundingBox(*rbox, meta={'city': city, 'side': 'r', 'text': text})
|
||||||
|
rbox.right += 1.5 * ns.font_distance
|
||||||
|
lbox = pil.textbbox((location[0] - ns.font_distance, location[1] - ns.font_size/2), text, font=font, anchor='rt')
|
||||||
|
lbox = BoundingBox(*lbox, meta={'city': city, 'side': 'l', 'text': text})
|
||||||
|
lbox.left -= 1.5 * ns.font_distance
|
||||||
|
if location[0] > 0.8 * size[0]:
|
||||||
|
rbox.base_weight += 0.001
|
||||||
|
else:
|
||||||
|
lbox.base_weight += 0.001
|
||||||
|
candidates[city] = [lbox, lbox + (0, ns.dotsize_erfa), lbox + (0, ns.dotsize_erfa*2), lbox - (0, ns.dotsize_erfa), lbox - (0, ns.dotsize_erfa*2), rbox, rbox + (0, ns.dotsize_erfa), rbox + (0, ns.dotsize_erfa*2), rbox - (0, ns.dotsize_erfa), rbox - (0, ns.dotsize_erfa*2)]
|
||||||
|
rbox.base_weight -= 0.003
|
||||||
|
lbox.base_weight -= 0.003
|
||||||
|
|
||||||
if ns.debug:
|
if ns.debug:
|
||||||
dr1 = etree.Element('rect', x=str(lbox.left), y=str(lbox.top), width=str(lbox.width), height=str(lbox.height))
|
for c in candidates[city]:
|
||||||
dr1.set('class', 'debugleft')
|
dr = etree.Element('rect', x=str(c.left), y=str(c.top), width=str(c.width), height=str(c.height))
|
||||||
dr2 = etree.Element('rect', x=str(rbox.left), y=str(rbox.top), width=str(rbox.width), height=str(rbox.height))
|
dr.set('class', 'debugleft')
|
||||||
dr2.set('class', 'debugright')
|
svg.append(dr)
|
||||||
svg.append(dr1)
|
|
||||||
svg.append(dr2)
|
|
||||||
|
|
||||||
|
|
||||||
unfinished = {c for c in erfas.keys()}
|
unfinished = {c for c in erfas.keys()}
|
||||||
finished = {}
|
finished = {}
|
||||||
for city in list(unfinished):
|
|
||||||
margin_left = lboxes[city].with_margin(ns.dotsize_erfa)
|
|
||||||
i_left = sum([1 for c, b in lboxes.items() if c != city and b in margin_left])
|
|
||||||
i_left += sum([1 for c, b in rboxes.items() if c != city and b in margin_left])
|
|
||||||
i_left += sum([0.5 for c, loc in chaostreffs.items() if loc in margin_left])
|
|
||||||
margin_right = rboxes[city].with_margin(ns.dotsize_erfa)
|
|
||||||
i_right = sum([1 for c, b in lboxes.items() if c != city and b in margin_right])
|
|
||||||
i_right += sum([1 for c, b in rboxes.items() if c != city and b in margin_right])
|
|
||||||
i_right += sum([0.5 for c, loc in chaostreffs.items() if loc in margin_right])
|
|
||||||
|
|
||||||
if i_right == 0 and i_left > 0:
|
while len(unfinished) > 0:
|
||||||
finished[city] = rboxes[city]
|
|
||||||
unfinished.discard(city)
|
|
||||||
elif i_left == 0:
|
|
||||||
if lboxes[city].left > size[0] * 0.8:
|
|
||||||
finished[city] = rboxes[city]
|
|
||||||
else:
|
|
||||||
finished[city] = lboxes[city]
|
|
||||||
unfinished.discard(city)
|
|
||||||
|
|
||||||
for a in list(unfinished):
|
all_boxes = set(finished.values())
|
||||||
if a not in unfinished:
|
unfinished_boxes = set()
|
||||||
|
for city in unfinished:
|
||||||
|
all_boxes.update(candidates[city])
|
||||||
|
unfinished_boxes.update(candidates[city])
|
||||||
|
for box in all_boxes:
|
||||||
|
box.compute_weight(all_boxes, erfas, chaostreffs, ns)
|
||||||
|
|
||||||
|
# Get candidate with the least number of optimal solutions > 0
|
||||||
|
optcity = min(unfinished, key=lambda city: len([1 for box in candidates[city] if box.is_optimal]) or float('inf'))
|
||||||
|
optboxes = [box for box in candidates[optcity] if box.is_optimal]
|
||||||
|
if len(optboxes) > 0:
|
||||||
|
finished[optcity] = min(optboxes, key=lambda box: box.weight)
|
||||||
|
finished[optcity].finished = True
|
||||||
|
unfinished.discard(optcity)
|
||||||
continue
|
continue
|
||||||
mla = lboxes[a].with_margin(ns.dotsize_erfa)
|
|
||||||
mra = rboxes[a].with_margin(ns.dotsize_erfa)
|
|
||||||
for b in list(unfinished.difference({a})):
|
|
||||||
if b not in unfinished:
|
|
||||||
continue
|
|
||||||
mlb = lboxes[b]
|
|
||||||
mrb = rboxes[b]
|
|
||||||
if mla not in mlb and mla not in mlb and mra not in mlb and mra not in mrb:
|
|
||||||
continue
|
|
||||||
if mra in mlb:
|
|
||||||
finished[a] = lboxes[a]
|
|
||||||
finished[b] = rboxes[b]
|
|
||||||
unfinished.discard(a)
|
|
||||||
unfinished.discard(b)
|
|
||||||
elif mla in mrb:
|
|
||||||
finished[a] = rboxes[a]
|
|
||||||
finished[b] = lboxes[b]
|
|
||||||
unfinished.discard(a)
|
|
||||||
unfinished.discard(b)
|
|
||||||
|
|
||||||
for city in list(unfinished):
|
# If no candidate with at least one optimal solution is left, go by global minimum
|
||||||
margin_left = lboxes[city].with_margin(ns.dotsize_erfa)
|
minbox = min(unfinished_boxes, key=lambda box: box.weight)
|
||||||
margin_right = rboxes[city].with_margin(ns.dotsize_erfa)
|
mincity = minbox.meta['city']
|
||||||
i_left = sum([1 for c, b in finished.items() if c != city and b in margin_left])
|
finished[mincity] = minbox
|
||||||
i_left += sum([0.5 for c, loc in chaostreffs.items() if loc in margin_left])
|
minbox.finished = True
|
||||||
i_right = sum([1 for c, b in finished.items() if c != city and b in margin_right])
|
unfinished.discard(mincity)
|
||||||
i_right += sum([0.5 for c, loc in chaostreffs.items() if loc in margin_right])
|
|
||||||
if i_left <= i_right:
|
# Iteratively improve the layout
|
||||||
finished[city] = lboxes[city]
|
for i in tqdm.tqdm(range(len(finished))):
|
||||||
else:
|
changed = False
|
||||||
finished[city] = rboxes[city]
|
order = list(finished.keys())
|
||||||
unfinished.discard(city)
|
random.shuffle(order)
|
||||||
|
for city in order:
|
||||||
|
all_boxes = set(finished.values())
|
||||||
|
for box in candidates[city]:
|
||||||
|
box.compute_finalized_weight(all_boxes, erfas, chaostreffs, ns)
|
||||||
|
minbox = min(candidates[city], key=lambda box: box.weight)
|
||||||
|
if minbox is not finished[city]:
|
||||||
|
changed = True
|
||||||
|
finished[city] = minbox
|
||||||
|
minbox.finished = True
|
||||||
|
if not changed:
|
||||||
|
# Nothing changed in this iteration - no need to retry
|
||||||
|
break
|
||||||
|
|
||||||
return finished
|
return finished
|
||||||
|
|
||||||
|
@ -384,6 +498,7 @@ def create_svg(ns, bbox):
|
||||||
with open(path, 'r') as sf:
|
with open(path, 'r') as sf:
|
||||||
shapedata = sf.read()
|
shapedata = sf.read()
|
||||||
shape = json.loads(shapedata)
|
shape = json.loads(shapedata)
|
||||||
|
name = shape['description']['en']
|
||||||
geo = shape['data']['features'][0]['geometry']
|
geo = shape['data']['features'][0]['geometry']
|
||||||
if geo['type'] == 'Polygon':
|
if geo['type'] == 'Polygon':
|
||||||
geo['coordinates'] = [geo['coordinates']]
|
geo['coordinates'] = [geo['coordinates']]
|
||||||
|
@ -392,7 +507,7 @@ def create_svg(ns, bbox):
|
||||||
for x, y in poly[0]:
|
for x, y in poly[0]:
|
||||||
xt, yt = transformer.transform(x, y)
|
xt, yt = transformer.transform(x, y)
|
||||||
ts.append((xt*scalex - origin[0], origin[1] - yt*scaley))
|
ts.append((xt*scalex - origin[0], origin[1] - yt*scaley))
|
||||||
shapes_states.append(ts)
|
shapes_states.append((name, ts))
|
||||||
|
|
||||||
shapes_countries = []
|
shapes_countries = []
|
||||||
files = os.listdir(ns.cache_directory.shapes_filtered)
|
files = os.listdir(ns.cache_directory.shapes_filtered)
|
||||||
|
@ -403,6 +518,7 @@ def create_svg(ns, bbox):
|
||||||
with open(path, 'r') as sf:
|
with open(path, 'r') as sf:
|
||||||
shapedata = sf.read()
|
shapedata = sf.read()
|
||||||
shape = json.loads(shapedata)
|
shape = json.loads(shapedata)
|
||||||
|
name = shape['description']['en']
|
||||||
geo = shape['data']['features'][0]['geometry']
|
geo = shape['data']['features'][0]['geometry']
|
||||||
if geo['type'] == 'Polygon':
|
if geo['type'] == 'Polygon':
|
||||||
geo['coordinates'] = [geo['coordinates']]
|
geo['coordinates'] = [geo['coordinates']]
|
||||||
|
@ -411,7 +527,7 @@ def create_svg(ns, bbox):
|
||||||
for x, y in poly[0]:
|
for x, y in poly[0]:
|
||||||
xt, yt = transformer.transform(x, y)
|
xt, yt = transformer.transform(x, y)
|
||||||
ts.append((xt*scalex - origin[0], origin[1] - yt*scaley))
|
ts.append((xt*scalex - origin[0], origin[1] - yt*scaley))
|
||||||
shapes_countries.append(ts)
|
shapes_countries.append((name, ts))
|
||||||
|
|
||||||
chaostreffs = {}
|
chaostreffs = {}
|
||||||
with open(ns.cache_directory.chaostreff_info, 'r') as f:
|
with open(ns.cache_directory.chaostreff_info, 'r') as f:
|
||||||
|
@ -432,7 +548,7 @@ def create_svg(ns, bbox):
|
||||||
erfas[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
|
erfas[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
|
||||||
|
|
||||||
rectbox = [0, 0, svg_box[0], svg_box[1]]
|
rectbox = [0, 0, svg_box[0], svg_box[1]]
|
||||||
for shape in shapes_states + shapes_countries:
|
for name, shape in shapes_states + shapes_countries:
|
||||||
for lon, lat in shape:
|
for lon, lat in shape:
|
||||||
rectbox[0] = min(lon, rectbox[0])
|
rectbox[0] = min(lon, rectbox[0])
|
||||||
rectbox[1] = min(lat, rectbox[1])
|
rectbox[1] = min(lat, rectbox[1])
|
||||||
|
@ -452,6 +568,7 @@ def create_svg(ns, bbox):
|
||||||
svg.append(defs)
|
svg.append(defs)
|
||||||
|
|
||||||
bg = etree.Element('rect',
|
bg = etree.Element('rect',
|
||||||
|
id='background',
|
||||||
x=str(rectbox[0]),
|
x=str(rectbox[0]),
|
||||||
y=str(rectbox[1]),
|
y=str(rectbox[1]),
|
||||||
width=str(rectbox[3]-rectbox[1]),
|
width=str(rectbox[3]-rectbox[1]),
|
||||||
|
@ -459,37 +576,44 @@ def create_svg(ns, bbox):
|
||||||
bg.set('class', 'background')
|
bg.set('class', 'background')
|
||||||
svg.append(bg)
|
svg.append(bg)
|
||||||
|
|
||||||
for shape in shapes_countries:
|
for name, shape in shapes_countries:
|
||||||
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
|
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
|
||||||
poly = etree.Element('polygon', points=points)
|
poly = etree.Element('polygon', points=points)
|
||||||
poly.set('class', 'country')
|
poly.set('class', 'country')
|
||||||
|
poly.set('data-country', name)
|
||||||
svg.append(poly)
|
svg.append(poly)
|
||||||
|
|
||||||
# Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
|
# Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
|
||||||
for shape in sorted(shapes_states, key=lambda x: -sum(len(s) for s in x)):
|
for name, shape in sorted(shapes_states, key=lambda x: -sum(len(s) for s in x[1])):
|
||||||
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
|
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
|
||||||
poly = etree.Element('polygon', points=points)
|
poly = etree.Element('polygon', points=points)
|
||||||
poly.set('class', 'state')
|
poly.set('class', 'state')
|
||||||
|
poly.set('data-state', name)
|
||||||
svg.append(poly)
|
svg.append(poly)
|
||||||
|
|
||||||
for city, location in erfas.items():
|
for city, location in erfas.items():
|
||||||
circle = etree.Element('circle', cx=str(location[0]), cy=str(location[1]), r=str(ns.dotsize_erfa))
|
circle = etree.Element('circle', cx=str(location[0]), cy=str(location[1]), r=str(ns.dotsize_erfa))
|
||||||
circle.set('class', 'erfa')
|
circle.set('class', 'erfa')
|
||||||
|
circle.set('data-erfa', city)
|
||||||
svg.append(circle)
|
svg.append(circle)
|
||||||
|
|
||||||
for city, location in chaostreffs.items():
|
for city, location in chaostreffs.items():
|
||||||
circle = etree.Element('circle', cx=str(location[0]), cy=str(location[1]), r=str(ns.dotsize_treff))
|
circle = etree.Element('circle', cx=str(location[0]), cy=str(location[1]), r=str(ns.dotsize_treff))
|
||||||
circle.set('class', 'chaostreff')
|
circle.set('class', 'chaostreff')
|
||||||
|
circle.set('data-chaostreff', city)
|
||||||
svg.append(circle)
|
svg.append(circle)
|
||||||
|
|
||||||
|
|
||||||
print('Layouting labels')
|
print('Layouting labels')
|
||||||
texts = optimize_text_layout(ns, erfas, chaostreffs, (int(svg_box[0]), int(svg_box[1])), svg)
|
texts = optimize_text_layout(ns, erfas, chaostreffs, (int(svg_box[0]), int(svg_box[1])), svg)
|
||||||
for city, box in texts.items():
|
for city, box in texts.items():
|
||||||
|
if box.meta['side'] == 'l':
|
||||||
|
box += (1.5 * ns.font_distance, 0)
|
||||||
text = etree.Element('text', x=str(box.left), y=str(box.top))
|
text = etree.Element('text', x=str(box.left), y=str(box.top))
|
||||||
text.set('alignment-baseline', 'hanging')
|
text.set('alignment-baseline', 'hanging')
|
||||||
text.set('class', 'erfalabel')
|
text.set('class', 'erfalabel')
|
||||||
text.text = city
|
text.set('data-erfa', city)
|
||||||
|
text.text = box.meta['text']
|
||||||
svg.append(text)
|
svg.append(text)
|
||||||
|
|
||||||
print('Done, writing SVG')
|
print('Done, writing SVG')
|
||||||
|
@ -502,20 +626,20 @@ def create_svg(ns, bbox):
|
||||||
print('Done')
|
print('Done')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(sys.argv[0])
|
ap = argparse.ArgumentParser(sys.argv[0])
|
||||||
ap.add_argument('--cache-directory', type=CachePaths, default='cache', help='Path to the cache directory')
|
ap.add_argument('--cache-directory', type=CachePaths, default='cache', help='Path to the cache directory')
|
||||||
ap.add_argument('--update-borders', action='store_true', default=False, help='Update country borders from Wikidata')
|
ap.add_argument('--update-borders', action='store_true', default=False, help='Update country borders from Wikidata')
|
||||||
ap.add_argument('--update-erfalist', action='store_true', default=False, help='Update Erfa/Chaostreff list from doku.ccc.de')
|
ap.add_argument('--update-erfalist', action='store_true', default=False, help='Update Erfa/Chaostreff list from doku.ccc.de')
|
||||||
ap.add_argument('--bbox', type=float, nargs=4, default=None, help='Override map bounding box with <lon> <lat> <lon> <lat>')
|
ap.add_argument('--bbox', type=float, nargs=4, metavar=('LON', 'LAT', 'LON', 'LAT') ,default=None, help='Override map bounding box')
|
||||||
ap.add_argument('--bbox-margin', type=float, default=0.3, help='Margin around the inferred bounding box. Ignored with --bbox')
|
ap.add_argument('--bbox-margin', type=float, default=0.3, help='Margin around the inferred bounding box. Ignored with --bbox')
|
||||||
ap.add_argument('--stylesheet', type=str, default='style/erfamap.css', help='Stylesheet for the generated SVG')
|
ap.add_argument('--stylesheet', type=str, default='style/erfamap.css', help='Stylesheet for the generated SVG')
|
||||||
ap.add_argument('--font', type=str, default='style/concertone-regular.ttf', help='Name of the font used in the stylesheet.')
|
ap.add_argument('--font', type=str, default='style/concertone-regular.ttf', help='Name of the font used in the stylesheet.')
|
||||||
ap.add_argument('--font-size', type=int, default=30, help='Size of the font used in the stylesheet.')
|
ap.add_argument('--font-size', type=int, default=40, help='Size of the font used in the stylesheet.')
|
||||||
ap.add_argument('--font-distance', type=int, default=25, help='Distance of labels from their dots center')
|
ap.add_argument('--font-distance', type=int, default=15, help='Distance of labels from their dots center')
|
||||||
ap.add_argument('--dotsize-erfa', type=float, default=13, help='Radius of Erfa dots')
|
ap.add_argument('--dotsize-erfa', type=float, default=13, help='Radius of Erfa dots')
|
||||||
ap.add_argument('--dotsize-treff', type=float, default=8, help='Radius of Chaostreff dots')
|
ap.add_argument('--dotsize-treff', type=float, default=8, help='Radius of Chaostreff dots')
|
||||||
|
ap.add_argument('--rename', type=str, action='append', nargs=2, metavar=('FROM', 'TO'), default=[], help='Rename a city with an overly long name (e.g. "Rothenburg ob der Tauber" to "Rothenburg")')
|
||||||
ap.add_argument('--projection', type=str, default='epsg:4258', help='Map projection to convert the WGS84 coordinates to')
|
ap.add_argument('--projection', type=str, default='epsg:4258', help='Map projection to convert the WGS84 coordinates to')
|
||||||
ap.add_argument('--scale-x', type=float, default=130, help='X axis scale to apply after projecting')
|
ap.add_argument('--scale-x', type=float, default=130, help='X axis scale to apply after projecting')
|
||||||
ap.add_argument('--scale-y', type=float, default=200, help='Y axis scale to apply after projecting')
|
ap.add_argument('--scale-y', type=float, default=200, help='Y axis scale to apply after projecting')
|
||||||
|
|
BIN
map.readme.png
BIN
map.readme.png
Binary file not shown.
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 412 KiB |
|
@ -29,7 +29,7 @@ circle.erfa, circle.chaostreff {
|
||||||
|
|
||||||
text.erfalabel {
|
text.erfalabel {
|
||||||
font-family: concertone;
|
font-family: concertone;
|
||||||
font-size: 30px;
|
font-size: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
rect.debugleft {
|
rect.debugleft {
|
||||||
|
|
Loading…
Reference in a new issue