2022-10-08 04:24:14 +02:00
#!/usr/bin/env python3
2022-10-09 02:26:47 +02:00
#
# https://git.kabelsalat.ch/s3lph/erfamap
2022-10-08 04:24:14 +02:00
import os
import urllib . request
2022-10-08 21:17:31 +02:00
import urllib . parse
2022-10-08 04:24:14 +02:00
import json
import base64
2022-10-08 21:17:31 +02:00
import sys
import argparse
import shutil
2022-10-09 07:12:43 +02:00
import random
2022-10-12 02:43:47 +02:00
import math
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
from lxml import etree
2022-10-08 04:24:14 +02:00
import pyproj
from geopy import Nominatim
2022-10-08 21:17:31 +02:00
import tqdm
import cairosvg
from PIL import Image , ImageFont
from PIL . ImageDraw import ImageDraw
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
USER_AGENT = ' Erfamap/v0.1 (https://git.kabelsalat.ch/s3lph/erfamap) '
class CachePaths :
def __init__ ( self , path : str ) :
if os . path . exists ( path ) and not os . path . isdir ( path ) :
raise AttributeError ( f ' Path exists but is not a directory: { path } ' )
os . makedirs ( path , exist_ok = True )
self . shapes_states = os . path . join ( path , ' shapes_states ' )
self . shapes_countries = os . path . join ( path , ' shapes_countries ' )
self . shapes_filtered = os . path . join ( path , ' shapes_filtered ' )
self . erfa_info = os . path . join ( path , ' erfa-info.json ' )
self . chaostreff_info = os . path . join ( path , ' chaostreff-info.json ' )
2022-10-12 02:43:47 +02:00
class OutputPaths :
def __init__ ( self , path : str ) :
if os . path . exists ( path ) and not os . path . isdir ( path ) :
raise AttributeError ( f ' Path exists but is not a directory: { path } ' )
os . makedirs ( path , exist_ok = True )
self . path = path
self . svg_path = os . path . join ( path , ' map.svg ' )
self . png_path = os . path . join ( path , ' map.png ' )
self . html_path = os . path . join ( path , ' erfamap.html ' )
self . imagemap_path = os . path . join ( path , ' imagemap.html ' )
def rel ( self , path : str ) :
return os . path . relpath ( path , start = self . path )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
ERFA_URL = ' https://doku.ccc.de/index.php?title=Spezial:Semantische_Suche&x=-5B-5BKategorie % 3AErfa-2DKreise-5D-5D-20-5B-5BChaostreff-2DActive % 3A % 3Awahr-5D-5D %2F -3FChaostreff-2DCity %2F -3FChaostreff-2DPhysical-2DAddress %2F -3FChaostreff-2DPhysical-2DHousenumber %2F -3FChaostreff-2DPhysical-2DPostcode %2F -3FChaostreff-2DPhysical-2DCity %2F -3FChaostreff-2DCountry %2F -3FPublic-2DWeb %2F -3FChaostreff-2DLongname %2F -3FChaostreff-2DNickname %2F -3FChaostreff-2DRealname&format=json&limit=200&link=all&headers=show&searchlabel=JSON&class=sortable+wikitable+smwtable&sort=&order=asc&offset=0&mainlabel=&prettyprint=true&unescape=true '
CHAOSTREFF_URL = ' https://doku.ccc.de/index.php?title=Spezial:Semantische_Suche&x=-5B-5BKategorie % 3AChaostreffs-5D-5D-20-5B-5BChaostreff-2DActive % 3A % 3Awahr-5D-5D %2F -3FChaostreff-2DCity %2F -3FChaostreff-2DPhysical-2DAddress %2F -3FChaostreff-2DPhysical-2DHousenumber %2F -3FChaostreff-2DPhysical-2DPostcode %2F -3FChaostreff-2DPhysical-2DCity %2F -3FChaostreff-2DCountry %2F -3FPublic-2DWeb %2F -3FChaostreff-2DLongname %2F -3FChaostreff-2DNickname %2F -3FChaostreff-2DRealname&format=json&limit=200&link=all&headers=show&searchlabel=JSON&class=sortable+wikitable+smwtable&sort=&order=asc&offset=0&mainlabel=&prettyprint=true&unescape=true '
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
def sparql_query ( query ) :
headers = {
' Content-Type ' : ' application/x-www-form-urlencoded ' ,
' User-Agent ' : USER_AGENT ,
' Accept ' : ' application/sparql-results+json ' ,
}
body = urllib . parse . urlencode ( { ' query ' : query } ) . encode ( )
req = urllib . request . Request ( ' https://query.wikidata.org/sparql ' , headers = headers , data = body )
with urllib . request . urlopen ( req ) as resp :
resultset = json . load ( resp )
results = { }
for r in resultset . get ( ' results ' , { } ) . get ( ' bindings ' ) :
basename = None
url = None
for k , v in r . items ( ) :
if k == ' item ' :
basename = os . path . basename ( v [ ' value ' ] )
elif k == ' map ' :
url = v [ ' value ' ]
results [ basename ] = url
return results
def fetch_geoshapes ( target , shape_urls ) :
os . makedirs ( target , exist_ok = True )
2022-10-09 01:15:36 +02:00
candidates = { }
keep = { }
2022-10-08 21:17:31 +02:00
for item , url in tqdm . tqdm ( shape_urls . items ( ) ) :
try :
with urllib . request . urlopen ( url ) as resp :
2022-10-09 01:15:36 +02:00
shape = json . load ( resp )
2022-10-09 02:26:47 +02:00
if not shape . get ( ' license ' , ' proprietary ' ) . startswith ( ' CC0- ' ) :
# Only include public domain data
continue
2022-10-09 01:15:36 +02:00
candidates . setdefault ( item , [ ] ) . append ( shape )
2022-10-08 21:17:31 +02:00
except urllib . error . HTTPError as e :
print ( e )
2022-10-09 01:15:36 +02:00
for item , ican in candidates . items ( ) :
# Prefer zoom level 4
keep [ item ] = min ( ican , key = lambda x : abs ( 4 - x . get ( ' zoom ' , 1000 ) ) )
for item , shape in keep . items ( ) :
with open ( os . path . join ( target , item + ' .json ' ) , ' w ' ) as f :
json . dump ( shape , f )
2022-10-08 21:17:31 +02:00
def fetch_wikidata_states ( target ) :
shape_urls = sparql_query ( '''
2022-10-08 04:24:14 +02:00
PREFIX wd : < http : / / www . wikidata . org / entity / >
PREFIX wdt : < http : / / www . wikidata . org / prop / direct / >
SELECT DISTINCT ? item ? map WHERE {
# ?item is instance of federal state of germany and has geoshape ?map
? item wdt : P31 wd : Q1221156 ;
wdt : P3896 ? map
}
''' )
2022-10-08 21:17:31 +02:00
print ( ' Retrieving state border shapes ' )
fetch_geoshapes ( target , shape_urls )
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
def fetch_wikidata_countries ( target ) :
shape_urls = sparql_query ( '''
2022-10-08 04:24:14 +02:00
PREFIX wd : < http : / / www . wikidata . org / entity / >
PREFIX wdt : < http : / / www . wikidata . org / prop / direct / >
2022-10-09 01:15:36 +02:00
PREFIX p : < http : / / www . wikidata . org / prop / >
PREFIX ps : < http : / / www . wikidata . org / prop / statement / >
PREFIX wikibase : < http : / / wikiba . se / ontology #>
2022-10-08 04:24:14 +02:00
SELECT DISTINCT ? item ? map WHERE {
# ?item is instance of sovereign state, transitively part of europe and has geoshape ?map
# ?item is instance of country or sovereign state
? item wdt : P31 ? stateclass .
# ?item is transitively part of Europe (Contintent) or EEA
2022-10-09 01:15:36 +02:00
? item wdt : P361 + ? euroclass .
# ?item has geoshape ?map (including all non-deprecated results)
? item p : P3896 ? st .
? st ps : P3896 ? map ;
MINUS { ? st wikibase : rank wikibase : DeprecatedRank }
2022-10-08 04:24:14 +02:00
FILTER ( ? stateclass = wd : Q6256 | | ? stateclass = wd : Q3624078 ) .
FILTER ( ? euroclass = wd : Q46 | | ? euroclass = wd : Q8932 ) .
}
''' )
2022-10-08 21:17:31 +02:00
print ( ' Retrieving country border shapes ' )
fetch_geoshapes ( target , shape_urls )
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
def filter_boundingbox ( source , target , bbox ) :
2022-10-08 04:24:14 +02:00
files = os . listdir ( source )
os . makedirs ( target , exist_ok = True )
2022-10-08 21:17:31 +02:00
print ( ' Filtering countries outside the bounding box ' )
for f in tqdm . tqdm ( files ) :
2022-10-08 04:24:14 +02:00
if not f . endswith ( ' .json ' ) or ' Q183.json ' in f :
continue
path = os . path . join ( source , f )
with open ( path , ' r ' ) as sf :
shapedata = sf . read ( )
shape = json . loads ( shapedata )
keep = False
geo = shape [ ' data ' ] [ ' features ' ] [ 0 ] [ ' geometry ' ]
if geo [ ' type ' ] == ' Polygon ' :
geo [ ' coordinates ' ] = [ geo [ ' coordinates ' ] ]
for poly in geo [ ' coordinates ' ] :
for point in poly [ 0 ] :
2022-10-08 21:17:31 +02:00
if point [ 0 ] > = bbox [ 0 ] [ 0 ] and point [ 1 ] > = bbox [ 0 ] [ 1 ] \
and point [ 0 ] < = bbox [ 1 ] [ 0 ] and point [ 1 ] < = bbox [ 1 ] [ 1 ] :
2022-10-08 04:24:14 +02:00
keep = True
break
if keep :
break
if keep :
with open ( os . path . join ( target , f ) , ' w ' ) as sf :
sf . write ( shapedata )
2022-10-08 21:17:31 +02:00
def address_lookup ( name , erfa ) :
locator = Nominatim ( user_agent = USER_AGENT )
number = erfa [ ' Chaostreff-Physical-Housenumber ' ]
street = erfa [ ' Chaostreff-Physical-Address ' ]
zipcode = erfa [ ' Chaostreff-Physical-Postcode ' ]
acity = erfa [ ' Chaostreff-Physical-City ' ]
city = erfa [ ' Chaostreff-City ' ] [ 0 ]
country = erfa [ ' Chaostreff-Country ' ] [ 0 ]
2022-10-12 02:43:47 +02:00
# Try the most accurate address first, try increasingly inaccurate addresses on failure.
2022-10-08 21:17:31 +02:00
formats = [
# Muttenz, Schweiz
f ' { city } , { country } '
]
if zipcode and acity :
# 4132 Muttenz, Schweiz
formats . insert ( 0 , f ' { zipcode [ 0 ] } { acity [ 0 ] } , { country } ' )
if zipcode and acity and number and street :
# Birsfelderstrasse 6, 4132 Muttenz, Schweiz
formats . insert ( 0 , f ' { street [ 0 ] } { number [ 0 ] } , { zipcode [ 0 ] } { acity [ 0 ] } , { country } ' )
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
for fmt in formats :
response = locator . geocode ( fmt )
if response is not None :
return response . longitude , response . latitude
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
print ( f ' No location found for { name } , tried the following address formats: ' )
for fmt in formats :
print ( f ' { fmt } ' )
return None
def fetch_erfas ( target , url ) :
2022-10-08 04:24:14 +02:00
userpw = os . getenv ( ' DOKU_CCC_DE_BASICAUTH ' )
if userpw is None :
print ( ' Please set environment variable DOKU_CCC_DE_BASICAUTH=username:password ' )
exit ( 1 )
auth = base64 . b64encode ( userpw . encode ( ) ) . decode ( )
erfas = { }
req = urllib . request . Request ( url , headers = { ' Authorization ' : f ' Basic { auth } ' } )
with urllib . request . urlopen ( req ) as resp :
erfadata = json . loads ( resp . read ( ) . decode ( ) )
2022-10-08 21:17:31 +02:00
print ( ' Looking up addresses ' )
for name , erfa in tqdm . tqdm ( erfadata [ ' results ' ] . items ( ) ) :
location = address_lookup ( name , erfa [ ' printouts ' ] )
2022-10-08 04:24:14 +02:00
if location is None :
print ( f ' WARNING: No location for { name } ' )
city = erfa [ ' printouts ' ] [ ' Chaostreff-City ' ] [ 0 ]
2022-10-11 02:29:45 +02:00
erfas [ city ] = { ' location ' : location }
if len ( erfa [ ' printouts ' ] [ ' Public-Web ' ] ) > 0 :
erfas [ city ] [ ' web ' ] = erfa [ ' printouts ' ] [ ' Public-Web ' ] [ 0 ]
if len ( erfa [ ' printouts ' ] [ ' Chaostreff-Longname ' ] ) > 0 :
erfas [ city ] [ ' name ' ] = erfa [ ' printouts ' ] [ ' Chaostreff-Longname ' ] [ 0 ]
2022-10-12 02:43:47 +02:00
elif len ( erfa [ ' printouts ' ] [ ' Chaostreff-Nickname ' ] ) > 0 :
erfas [ city ] [ ' name ' ] = erfa [ ' printouts ' ] [ ' Chaostreff-Nickname ' ] [ 0 ]
elif len ( erfa [ ' printouts ' ] [ ' Chaostreff-Realname ' ] ) > 0 :
erfas [ city ] [ ' name ' ] = erfa [ ' printouts ' ] [ ' Chaostreff-Realname ' ] [ 0 ]
else :
erfas [ city ] [ ' name ' ] = name
2022-10-11 02:29:45 +02:00
2022-10-08 04:24:14 +02:00
with open ( target , ' w ' ) as f :
json . dump ( erfas , f )
2022-10-08 21:17:31 +02:00
def compute_bbox ( ns ) :
if ns . bbox is not None :
return [
( min ( ns . bbox [ 0 ] , ns . bbox [ 2 ] ) , min ( ns . bbox [ 1 ] , ns . bbox [ 3 ] ) ) ,
( max ( ns . bbox [ 0 ] , ns . bbox [ 2 ] ) , max ( ns . bbox [ 1 ] , ns . bbox [ 3 ] ) )
]
print ( ' Computing map bounding box ' )
bounds = [ ]
for path in tqdm . tqdm ( [ ns . cache_directory . erfa_info , ns . cache_directory . chaostreff_info ] ) :
with open ( path , ' r ' ) as f :
erfadata = json . load ( f )
2022-10-11 02:29:45 +02:00
for data in erfadata . values ( ) :
if ' location ' not in data :
continue
lon , lat = data [ ' location ' ]
2022-10-08 21:17:31 +02:00
if len ( bounds ) == 0 :
bounds . append ( lon )
bounds . append ( lat )
bounds . append ( lon )
bounds . append ( lat )
else :
bounds [ 0 ] = min ( bounds [ 0 ] , lon )
bounds [ 1 ] = min ( bounds [ 1 ] , lat )
bounds [ 2 ] = max ( bounds [ 2 ] , lon )
bounds [ 3 ] = max ( bounds [ 3 ] , lat )
return [
( bounds [ 0 ] - ns . bbox_margin , bounds [ 1 ] - ns . bbox_margin ) ,
( bounds [ 2 ] + ns . bbox_margin , bounds [ 3 ] + ns . bbox_margin )
]
class BoundingBox :
2022-10-09 22:40:01 +02:00
def __init__ ( self , left , top , right = None , bottom = None , * , width = None , height = None , meta = None , base_weight = 0 ) :
2022-10-08 21:17:31 +02:00
self . left = left
self . top = top
2022-10-09 22:40:01 +02:00
self . right = right if width is None else left + width
self . bottom = bottom if height is None else top + height
2022-10-09 07:12:43 +02:00
self . meta = meta
self . base_weight = base_weight
self . finished = False
2022-10-12 02:43:47 +02:00
self . _weight = 0
self . _optimal = True
2022-10-08 21:17:31 +02:00
def __contains__ ( self , other ) :
if isinstance ( other , BoundingBox ) :
if other . right < self . left or other . left > self . right :
return False
if other . bottom < self . top or other . top > self . bottom :
return False
return True
elif isinstance ( other , tuple ) or isinstance ( other , list ) :
if len ( other ) != 2 :
raise TypeError ( )
if self . left > other [ 0 ] or self . right < other [ 0 ] :
return False
if self . top > other [ 1 ] or self . bottom < other [ 1 ] :
return False
return True
raise TypeError ( )
2022-10-09 07:12:43 +02:00
def __add__ ( self , other ) :
if not isinstance ( other , tuple ) and not isinstance ( other , list ) :
raise TypeError ( )
2022-10-09 22:40:01 +02:00
return BoundingBox ( self . left + other [ 0 ] , self . top + other [ 1 ] ,
self . right + other [ 0 ] , self . bottom + other [ 1 ] ,
width = None , height = None , meta = self . meta , base_weight = self . base_weight )
2022-10-09 07:12:43 +02:00
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 ( )
2022-10-09 22:40:01 +02:00
return BoundingBox ( self . left - other [ 0 ] , self . top - other [ 1 ] ,
self . right - other [ 0 ] , self . bottom - other [ 1 ] ,
width = None , height = None , meta = self . meta , base_weight = self . base_weight )
2022-10-09 07:12:43 +02:00
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
2022-10-08 21:17:31 +02:00
@property
def width ( self ) :
return self . right - self . left
@property
def height ( self ) :
return self . bottom - self . top
2022-10-09 07:12:43 +02:00
@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
2022-10-12 02:43:47 +02:00
return math . sqrt ( ( c1 [ 0 ] - c2 [ 0 ] ) * * 2 + ( c1 [ 1 ] - c2 [ 1 ] ) * * 2 )
2022-10-09 07:12:43 +02:00
2022-10-08 21:17:31 +02:00
def with_margin ( self , margin ) :
2022-10-09 22:40:01 +02:00
return BoundingBox ( self . left - margin , self . top - margin ,
self . right + margin , self . bottom + margin ,
width = None , height = None , meta = self . meta , base_weight = self . base_weight )
2022-10-12 02:43:47 +02:00
def chebyshev_distance ( self , other ) :
# https://en.wikipedia.org/wiki/Chebyshev_distance
2022-10-09 22:40:01 +02:00
if other in self :
return 0
if isinstance ( other , BoundingBox ) :
if other . left < self . left < other . right or other . left < self . right < other . right :
dh = 0
else :
dh = min ( abs ( self . left - other . right ) , abs ( self . right - other . left ) )
if other . top < self . top < other . bottom or other . top < self . bottom < other . bottom :
dv = 0
else :
dv = min ( abs ( self . top - other . bottom ) , abs ( self . bottom - other . top ) )
return max ( dh , dv )
elif isinstance ( other , tuple ) or isinstance ( other , list ) :
if self . left < other [ 0 ] < self . right :
dh = 0
else :
dh = min ( abs ( self . left - other [ 0 ] ) , abs ( self . right - other [ 0 ] ) )
if self . top < other [ 1 ] < self . bottom :
dv = 0
else :
dv = min ( abs ( self . top - other [ 1 ] ) , abs ( self . bottom - other [ 1 ] ) )
return max ( dh , dv )
else :
raise TypeError ( )
2022-10-09 07:12:43 +02:00
2022-10-09 23:50:04 +02:00
def compute_weight ( self , other , erfas , chaostreffs , pdist , ns ) :
2022-10-09 07:12:43 +02:00
w = 0
2022-10-12 02:43:47 +02:00
self . _optimal = True
2022-10-09 23:50:04 +02:00
swm = self . with_margin ( pdist )
2022-10-09 07:12:43 +02:00
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 )
2022-10-09 22:40:01 +02:00
# I hope these weights are somewhat reasonably balanced...
2022-10-12 02:43:47 +02:00
# Basically the weights correspond to geometrical distances,
# except for an actual collision, which gets a huge extra weight.
2022-10-09 07:12:43 +02:00
for o in other :
if o . meta [ ' city ' ] == self . meta [ ' city ' ] :
continue
if o in self :
if o . finished :
2022-10-09 22:40:01 +02:00
w + = 1000
2022-10-12 02:43:47 +02:00
self . _optimal = False
2022-10-09 07:12:43 +02:00
else :
2022-10-09 22:40:01 +02:00
w + = 50
2022-10-12 02:43:47 +02:00
self . _optimal = False
2022-10-09 22:40:01 +02:00
else :
2022-10-12 02:43:47 +02:00
w + = max ( pdist * 2 - swm . chebyshev_distance ( o ) , 0 )
2022-10-09 22:40:01 +02:00
continue
2022-10-09 07:12:43 +02:00
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 :
2022-10-09 22:40:01 +02:00
w + = 1000
2022-10-12 02:43:47 +02:00
self . _optimal = False
2022-10-09 22:40:01 +02:00
else :
2022-10-09 23:50:04 +02:00
w + = max ( pdist * 2 - swe . chebyshev_distance ( location ) , 0 )
2022-10-09 07:12:43 +02:00
for city , location in chaostreffs . items ( ) :
if city == self . meta [ ' city ' ] :
continue
if location in swc :
2022-10-09 22:40:01 +02:00
w + = 1000
2022-10-12 02:43:47 +02:00
self . _optimal = False
2022-10-09 22:40:01 +02:00
else :
2022-10-09 23:50:04 +02:00
w + = max ( pdist * 2 - swc . chebyshev_distance ( location ) , 0 )
2022-10-09 07:12:43 +02:00
self . _weight = w
@property
def weight ( self ) :
return self . _weight + self . base_weight
@property
def is_optimal ( self ) :
2022-10-12 02:43:47 +02:00
# Candidate is considered optimal if it doesn't collide with anything else
return self . _optimal
2022-10-08 21:17:31 +02:00
2022-10-09 22:40:01 +02:00
def __str__ ( self ) :
return f ' (( { int ( self . left ) } , { int ( self . top ) } , { int ( self . right ) } , { int ( self . bottom ) } ), weight= { self . weight } ) '
2022-10-08 21:17:31 +02:00
def optimize_text_layout ( ns , erfas , chaostreffs , size , svg ) :
2022-10-12 02:43:47 +02:00
# Load the font and measure its various different heights
2022-10-08 21:17:31 +02:00
font = ImageFont . truetype ( ns . font , ns . font_size )
2022-10-09 22:40:01 +02:00
pil = ImageDraw ( Image . new ( ' P ' , ( 1 , 1 ) ) )
xheight = - pil . textbbox ( ( 0 , 0 ) , ' x ' , font = font , anchor = ' ls ' ) [ 1 ]
capheight = - pil . textbbox ( ( 0 , 0 ) , ' A ' , font = font , anchor = ' ls ' ) [ 1 ]
2022-10-12 02:43:47 +02:00
voffset = - capheight / 2
2022-10-08 21:17:31 +02:00
2022-10-12 02:43:47 +02:00
# Generate a discrete set of text placement candidates around each erfa dot
2022-10-09 07:12:43 +02:00
candidates = { }
2022-10-08 21:17:31 +02:00
for city , location in erfas . items ( ) :
2022-10-09 07:12:43 +02:00
text = city
2022-10-09 22:40:01 +02:00
erfax , erfay = location
2022-10-09 07:12:43 +02:00
for rfrom , to in ns . rename :
if rfrom == city :
text = to
break
2022-10-09 22:40:01 +02:00
meta = { ' city ' : city , ' text ' : text , ' baseline ' : capheight }
textbox = pil . textbbox ( ( 0 , 0 ) , text , font = font , anchor = ' ls ' ) # left, baseline at 0,0
mw , mh = textbox [ 2 ] - textbox [ 0 ] , textbox [ 3 ] - textbox [ 1 ]
2022-10-12 02:43:47 +02:00
candidates [ city ] = [ ]
# Iterate over the dot-to-text distance range in discrete steps
it = max ( 0 , ns . font_step_distance )
for j in range ( it + 1 ) :
dist = ns . font_min_distance + ( ns . font_max_distance - ns . font_min_distance ) * j / it
bw = dist
# Generate 5 candidates each left and right of the dot, with varying vertical offset
for i in range ( - 1 , 2 ) :
if i == 0 :
bw - = 0.003
bwl , bwr = bw , bw
if erfax > 0.8 * size [ 0 ] :
bwr = bw + 0.001
else :
bwl = bw + 0.001
candidates [ city ] . append ( BoundingBox ( erfax - dist - mw , erfay + voffset + ns . dotsize_erfa * i * 2 , width = mw , height = mh , meta = meta , base_weight = bwl ) )
candidates [ city ] . append ( BoundingBox ( erfax + dist , erfay + voffset + ns . dotsize_erfa * i * 2 , width = mw , height = mh , meta = meta , base_weight = bwr ) )
# Generate 3 candidates each above and beneath the dot, aligned left, centered and right
candidates [ city ] . extend ( [
BoundingBox ( erfax - mw / 2 , erfay - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
BoundingBox ( erfax - mw / 2 , erfay + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
BoundingBox ( erfax - ns . dotsize_erfa , erfay - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.002 ) ,
BoundingBox ( erfax - ns . dotsize_erfa , erfay + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.003 ) ,
BoundingBox ( erfax + ns . dotsize_erfa - mw , erfay - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
BoundingBox ( erfax + ns . dotsize_erfa - mw , erfay + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
] )
# If debugging is enabled, render one rectangle around each label's bounding box, and one rectangle around each label's median box
2022-10-08 21:17:31 +02:00
if ns . debug :
2022-10-09 07:12:43 +02:00
for c in candidates [ city ] :
2022-10-09 22:40:01 +02:00
di = etree . Element ( ' rect ' , x = str ( c . left ) , y = str ( c . top + capheight - xheight ) , width = str ( c . width ) , height = str ( xheight ) )
di . set ( ' class ' , ' debugright ' )
2022-10-09 07:12:43 +02:00
dr = etree . Element ( ' rect ' , x = str ( c . left ) , y = str ( c . top ) , width = str ( c . width ) , height = str ( c . height ) )
dr . set ( ' class ' , ' debugleft ' )
2022-10-09 22:40:01 +02:00
svg . append ( di )
2022-10-09 07:12:43 +02:00
svg . append ( dr )
2022-10-08 21:17:31 +02:00
unfinished = { c for c in erfas . keys ( ) }
finished = { }
2022-10-12 02:43:47 +02:00
# Greedily choose a candidate for each label
2022-10-09 22:40:01 +02:00
with tqdm . tqdm ( total = len ( erfas ) ) as progress :
while len ( unfinished ) > 0 :
progress . update ( len ( erfas ) - len ( unfinished ) )
2022-10-09 07:12:43 +02:00
2022-10-09 22:40:01 +02:00
all_boxes = set ( finished . values ( ) )
unfinished_boxes = set ( )
for city in unfinished :
all_boxes . update ( candidates [ city ] )
unfinished_boxes . update ( candidates [ city ] )
for box in all_boxes :
2022-10-12 02:43:47 +02:00
box . compute_weight ( all_boxes , erfas , chaostreffs , pdist = max ( ns . font_min_distance , xheight ) , ns = ns )
2022-10-09 22:40:01 +02:00
# 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
# If no candidate with at least one optimal solution is left, go by global minimum
minbox = min ( unfinished_boxes , key = lambda box : box . weight )
mincity = minbox . meta [ ' city ' ]
finished [ mincity ] = minbox
minbox . finished = True
unfinished . discard ( mincity )
2022-10-09 07:12:43 +02:00
# Iteratively improve the layout
for i in tqdm . tqdm ( range ( len ( finished ) ) ) :
changed = False
order = list ( finished . keys ( ) )
random . shuffle ( order )
for city in order :
all_boxes = set ( finished . values ( ) )
for box in candidates [ city ] :
2022-10-12 02:43:47 +02:00
box . compute_weight ( all_boxes , erfas , chaostreffs , pdist = max ( ns . font_min_distance , xheight ) , ns = ns )
2022-10-09 07:12:43 +02:00
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
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
return finished
2022-10-12 02:43:47 +02:00
def create_imagemap ( ns , size , parent ,
erfas , erfa_urls , erfa_names , texts ,
chaostreffs , chaostreff_urls , chaostreff_names ) :
2022-10-12 03:10:17 +02:00
s = ns . png_scale
2022-10-12 02:43:47 +02:00
img = etree . Element ( ' img ' ,
src = ns . output_directory . rel ( ns . output_directory . png_path ) ,
usemap = ' #erfamap ' ,
2022-10-12 03:10:17 +02:00
width = str ( size [ 0 ] * s ) , height = str ( size [ 1 ] * s ) )
2022-10-12 02:43:47 +02:00
imgmap = etree . Element ( ' map ' , name = ' erfamap ' )
for city , location in erfas . items ( ) :
if city not in erfa_urls :
continue
box = texts [ city ]
area = etree . Element ( ' area ' ,
shape = ' circle ' ,
2022-10-12 03:10:17 +02:00
coords = f ' { location [ 0 ] * s } , { location [ 1 ] * s } , { ns . dotsize_erfa * s } ' ,
2022-10-12 02:43:47 +02:00
href = erfa_urls [ city ] )
area2 = etree . Element ( ' area ' ,
shape = ' rect ' ,
2022-10-12 03:10:17 +02:00
coords = f ' { box . left * s } , { box . top * s } , { box . right * s } , { box . bottom * s } ' ,
2022-10-12 02:43:47 +02:00
href = erfa_urls [ city ] )
if city in erfa_names :
area . set ( ' title ' , erfa_names [ city ] )
area2 . set ( ' title ' , erfa_names [ city ] )
imgmap . append ( area )
imgmap . append ( area2 )
for city , location in chaostreffs . items ( ) :
if city not in chaostreff_urls :
continue
area = etree . Element ( ' area ' ,
shape = ' circle ' ,
2022-10-12 03:10:17 +02:00
coords = f ' { location [ 0 ] * s } , { location [ 1 ] * s } , { ns . dotsize_treff * s } ' ,
2022-10-12 02:43:47 +02:00
href = chaostreff_urls [ city ] )
if city in chaostreff_names :
area . set ( ' title ' , chaostreff_names [ city ] )
imgmap . append ( area )
parent . append ( img )
parent . append ( imgmap )
2022-10-08 21:17:31 +02:00
def create_svg ( ns , bbox ) :
print ( ' Creating SVG image ' )
# Convert from WGS84 lon, lat to chosen projection
transformer = pyproj . Transformer . from_crs ( ' epsg:4326 ' , ns . projection )
scalex = ns . scale_x
scaley = ns . scale_y
blt = transformer . transform ( * bbox [ 0 ] )
trt = transformer . transform ( * bbox [ 1 ] )
trans_bounding_box = [
2022-10-08 04:24:14 +02:00
( scalex * blt [ 0 ] , scaley * trt [ 1 ] ) ,
( scalex * trt [ 0 ] , scaley * blt [ 1 ] )
]
2022-10-08 21:17:31 +02:00
origin = trans_bounding_box [ 0 ]
svg_box = ( trans_bounding_box [ 1 ] [ 0 ] - origin [ 0 ] , origin [ 1 ] - trans_bounding_box [ 1 ] [ 1 ] )
2022-10-12 02:43:47 +02:00
# Load state border lines from cached JSON files
2022-10-08 04:24:14 +02:00
shapes_states = [ ]
2022-10-08 21:17:31 +02:00
files = os . listdir ( ns . cache_directory . shapes_states )
2022-10-08 04:24:14 +02:00
for f in files :
if not f . endswith ( ' .json ' ) :
continue
2022-10-08 21:17:31 +02:00
path = os . path . join ( ns . cache_directory . shapes_states , f )
2022-10-08 04:24:14 +02:00
with open ( path , ' r ' ) as sf :
shapedata = sf . read ( )
shape = json . loads ( shapedata )
2022-10-09 07:12:43 +02:00
name = shape [ ' description ' ] [ ' en ' ]
2022-10-08 04:24:14 +02:00
geo = shape [ ' data ' ] [ ' features ' ] [ 0 ] [ ' geometry ' ]
if geo [ ' type ' ] == ' Polygon ' :
geo [ ' coordinates ' ] = [ geo [ ' coordinates ' ] ]
for poly in geo [ ' coordinates ' ] :
ts = [ ]
for x , y in poly [ 0 ] :
xt , yt = transformer . transform ( x , y )
ts . append ( ( xt * scalex - origin [ 0 ] , origin [ 1 ] - yt * scaley ) )
2022-10-09 07:12:43 +02:00
shapes_states . append ( ( name , ts ) )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# Load country border lines from cached JSON files
2022-10-08 04:24:14 +02:00
shapes_countries = [ ]
2022-10-08 21:17:31 +02:00
files = os . listdir ( ns . cache_directory . shapes_filtered )
2022-10-08 04:24:14 +02:00
for f in files :
if not f . endswith ( ' .json ' ) :
continue
2022-10-08 21:17:31 +02:00
path = os . path . join ( ns . cache_directory . shapes_filtered , f )
2022-10-08 04:24:14 +02:00
with open ( path , ' r ' ) as sf :
shapedata = sf . read ( )
shape = json . loads ( shapedata )
2022-10-09 07:12:43 +02:00
name = shape [ ' description ' ] [ ' en ' ]
2022-10-08 04:24:14 +02:00
geo = shape [ ' data ' ] [ ' features ' ] [ 0 ] [ ' geometry ' ]
if geo [ ' type ' ] == ' Polygon ' :
geo [ ' coordinates ' ] = [ geo [ ' coordinates ' ] ]
for poly in geo [ ' coordinates ' ] :
ts = [ ]
for x , y in poly [ 0 ] :
xt , yt = transformer . transform ( x , y )
ts . append ( ( xt * scalex - origin [ 0 ] , origin [ 1 ] - yt * scaley ) )
2022-10-09 07:12:43 +02:00
shapes_countries . append ( ( name , ts ) )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# Load Erfa infos from cached JSON files
2022-10-09 23:01:04 +02:00
erfas = { }
2022-10-11 02:29:45 +02:00
erfa_urls = { }
erfa_names = { }
2022-10-09 23:01:04 +02:00
with open ( ns . cache_directory . erfa_info , ' r ' ) as f :
2022-10-08 04:24:14 +02:00
ctdata = json . load ( f )
2022-10-11 02:29:45 +02:00
for city , data in ctdata . items ( ) :
location = data . get ( ' location ' )
2022-10-08 04:24:14 +02:00
if location is None :
continue
xt , yt = transformer . transform ( * location )
2022-10-09 23:01:04 +02:00
erfas [ city ] = ( xt * scalex - origin [ 0 ] , origin [ 1 ] - yt * scaley )
2022-10-11 02:29:45 +02:00
web = data . get ( ' web ' )
if web is not None :
erfa_urls [ city ] = web
name = data . get ( ' name ' )
if name is not None :
erfa_names [ city ] = name
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# Load Chaostreff infos from cached JSON files
2022-10-09 23:01:04 +02:00
chaostreffs = { }
2022-10-11 02:29:45 +02:00
chaostreff_urls = { }
chaostreff_names = { }
2022-10-09 23:01:04 +02:00
with open ( ns . cache_directory . chaostreff_info , ' r ' ) as f :
2022-10-08 04:24:14 +02:00
ctdata = json . load ( f )
2022-10-11 02:29:45 +02:00
for city , data in ctdata . items ( ) :
location = data . get ( ' location ' )
2022-10-08 04:24:14 +02:00
if location is None :
continue
2022-10-09 23:01:04 +02:00
if city in erfas :
# There is an edge case where when a space changed states between Erfa and Chaostreff, the
# Semantic MediaWiki engine returns this space as both an Erfa and a Chaostreff, resulting
# in glitches in the rendering. As a workaround, here we simply assume that it's an Erfa.
continue
2022-10-08 04:24:14 +02:00
xt , yt = transformer . transform ( * location )
2022-10-09 23:01:04 +02:00
chaostreffs [ city ] = ( xt * scalex - origin [ 0 ] , origin [ 1 ] - yt * scaley )
2022-10-11 02:29:45 +02:00
web = data . get ( ' web ' )
if web is not None :
chaostreff_urls [ city ] = web
name = data . get ( ' name ' )
if name is not None :
chaostreff_names [ city ] = name
2022-10-08 04:24:14 +02:00
rectbox = [ 0 , 0 , svg_box [ 0 ] , svg_box [ 1 ] ]
2022-10-09 07:12:43 +02:00
for name , shape in shapes_states + shapes_countries :
2022-10-08 04:24:14 +02:00
for lon , lat in shape :
rectbox [ 0 ] = min ( lon , rectbox [ 0 ] )
rectbox [ 1 ] = min ( lat , rectbox [ 1 ] )
rectbox [ 2 ] = max ( lon , rectbox [ 2 ] )
rectbox [ 3 ] = max ( lat , rectbox [ 3 ] )
2022-10-12 02:43:47 +02:00
print ( ' Copying stylesheet and font ' )
dst = os . path . join ( ns . output_directory . path , ns . stylesheet )
os . makedirs ( os . path . dirname ( dst ) , exist_ok = True )
shutil . copyfile ( ns . stylesheet , dst )
dst = os . path . join ( ns . output_directory . path , ns . font )
os . makedirs ( os . path . dirname ( dst ) , exist_ok = True )
shutil . copyfile ( ns . font , dst )
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
svg = etree . Element ( ' svg ' ,
xmlns = ' http://www.w3.org/2000/svg ' ,
viewBox = f ' 0 0 { svg_box [ 0 ] } { svg_box [ 1 ] } ' ,
width = str ( svg_box [ 0 ] ) , height = str ( svg_box [ 1 ] ) )
2022-10-08 04:24:14 +02:00
2022-10-08 21:17:31 +02:00
style = etree . Element ( ' style ' , type = ' text/css ' )
style . text = f ' @import url( { ns . stylesheet } ) '
2022-10-12 02:43:47 +02:00
svg . append ( style )
2022-10-08 21:17:31 +02:00
bg = etree . Element ( ' rect ' ,
2022-10-09 07:12:43 +02:00
id = ' background ' ,
2022-10-08 21:17:31 +02:00
x = str ( rectbox [ 0 ] ) ,
y = str ( rectbox [ 1 ] ) ,
width = str ( rectbox [ 3 ] - rectbox [ 1 ] ) ,
height = str ( rectbox [ 2 ] - rectbox [ 0 ] ) )
bg . set ( ' class ' , ' background ' )
svg . append ( bg )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# Render country borders
2022-10-09 07:12:43 +02:00
for name , shape in shapes_countries :
2022-10-08 04:24:14 +02:00
points = ' ' . join ( [ f ' { lon } , { lat } ' for lon , lat in shape ] )
2022-10-08 21:17:31 +02:00
poly = etree . Element ( ' polygon ' , points = points )
poly . set ( ' class ' , ' country ' )
2022-10-09 07:12:43 +02:00
poly . set ( ' data-country ' , name )
2022-10-08 21:17:31 +02:00
svg . append ( poly )
2022-10-12 02:43:47 +02:00
# Render state borders
2022-10-08 21:17:31 +02:00
# Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
2022-10-09 07:12:43 +02:00
for name , shape in sorted ( shapes_states , key = lambda x : - sum ( len ( s ) for s in x [ 1 ] ) ) :
2022-10-08 21:17:31 +02:00
points = ' ' . join ( [ f ' { lon } , { lat } ' for lon , lat in shape ] )
poly = etree . Element ( ' polygon ' , points = points )
poly . set ( ' class ' , ' state ' )
2022-10-09 07:12:43 +02:00
poly . set ( ' data-state ' , name )
2022-10-08 21:17:31 +02:00
svg . append ( poly )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# This can take some time, especially if lots of candidates are generated
print ( ' Layouting labels ' )
texts = optimize_text_layout ( ns , erfas , chaostreffs , ( int ( svg_box [ 0 ] ) , int ( svg_box [ 1 ] ) ) , svg )
# Render Erfa dots and their labels
2022-10-08 04:24:14 +02:00
for city , location in erfas . items ( ) :
2022-10-11 02:29:45 +02:00
box = texts [ city ]
if city in erfa_urls :
group = etree . Element ( ' a ' , href = erfa_urls [ city ] , target = ' _blank ' )
else :
group = etree . Element ( ' g ' )
group . set ( ' data-erfa ' , city )
2022-10-08 21:17:31 +02:00
circle = etree . Element ( ' circle ' , cx = str ( location [ 0 ] ) , cy = str ( location [ 1 ] ) , r = str ( ns . dotsize_erfa ) )
circle . set ( ' class ' , ' erfa ' )
2022-10-09 07:12:43 +02:00
circle . set ( ' data-erfa ' , city )
2022-10-11 02:29:45 +02:00
if city in erfa_names :
title = etree . Element ( ' title ' )
title . text = erfa_names [ city ]
circle . append ( title )
group . append ( circle )
text = etree . Element ( ' text ' , x = str ( box . left ) , y = str ( box . top + box . meta [ ' baseline ' ] ) )
text . set ( ' class ' , ' erfalabel ' )
text . set ( ' data-erfa ' , city )
text . text = box . meta [ ' text ' ]
group . append ( text )
svg . append ( group )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
# Render Chaostreff dots
2022-10-08 21:17:31 +02:00
for city , location in chaostreffs . items ( ) :
circle = etree . Element ( ' circle ' , cx = str ( location [ 0 ] ) , cy = str ( location [ 1 ] ) , r = str ( ns . dotsize_treff ) )
circle . set ( ' class ' , ' chaostreff ' )
2022-10-09 07:12:43 +02:00
circle . set ( ' data-chaostreff ' , city )
2022-10-11 02:29:45 +02:00
if city in chaostreff_names :
title = etree . Element ( ' title ' )
title . text = chaostreff_names [ city ]
circle . append ( title )
if city in chaostreff_urls :
2022-10-12 02:43:47 +02:00
a = etree . Element ( ' a ' , href = chaostreff_urls [ city ] , target = ' _blank ' )
2022-10-11 02:29:45 +02:00
a . append ( circle )
svg . append ( a )
else :
svg . append ( circle )
2022-10-08 21:17:31 +02:00
2022-10-12 02:43:47 +02:00
# Generate SVG, PNG and HTML output files
print ( ' Writing SVG ' )
with open ( ns . output_directory . svg_path , ' wb ' ) as mapfile :
2022-10-08 21:17:31 +02:00
root = etree . ElementTree ( svg )
root . write ( mapfile )
2022-10-12 02:43:47 +02:00
print ( ' Writing PNG ' )
2022-10-12 03:10:17 +02:00
cairosvg . svg2png ( url = ns . output_directory . svg_path , write_to = ns . output_directory . png_path ,
scale = ns . png_scale )
2022-10-12 02:43:47 +02:00
print ( ' Writing HTML SVG page ' )
html = etree . Element ( ' html ' )
head = etree . Element ( ' head ' )
link = etree . Element ( ' link ' , rel = ' stylesheet ' , href = ns . stylesheet )
head . append ( link )
html . append ( head )
body = etree . Element ( ' body ' )
obj = etree . Element ( ' object ' ,
data = ns . output_directory . rel ( ns . output_directory . svg_path ) ,
width = str ( svg_box [ 0 ] ) , height = str ( svg_box [ 1 ] ) )
create_imagemap ( ns , svg_box , obj ,
erfas , erfa_urls , erfa_names , texts ,
chaostreffs , chaostreff_urls , chaostreff_names )
body . append ( obj )
html . append ( body )
with open ( ns . output_directory . html_path , ' wb ' ) as f :
f . write ( b ' <!DOCTYLE html> \n ' )
etree . ElementTree ( html ) . write ( f )
print ( ' Writing HTML Image Map ' )
html = etree . Element ( ' html ' )
2022-10-12 03:10:17 +02:00
head = etree . Element ( ' head ' )
link = etree . Element ( ' link ' , rel = ' stylesheet ' , href = ns . stylesheet )
head . append ( link )
html . append ( head )
2022-10-12 02:43:47 +02:00
body = etree . Element ( ' body ' )
html . append ( body )
create_imagemap ( ns , svg_box , body ,
erfas , erfa_urls , erfa_names , texts ,
chaostreffs , chaostreff_urls , chaostreff_names )
with open ( ns . output_directory . imagemap_path , ' wb ' ) as f :
f . write ( b ' <!DOCTYLE html> \n ' )
etree . ElementTree ( html ) . write ( f )
2022-10-08 21:17:31 +02:00
def main ( ) :
ap = argparse . ArgumentParser ( sys . argv [ 0 ] )
ap . add_argument ( ' --cache-directory ' , type = CachePaths , default = ' cache ' , help = ' Path to the cache directory ' )
2022-10-12 02:43:47 +02:00
ap . add_argument ( ' --output-directory ' , type = OutputPaths , default = ' out ' , help = ' Path to the output directory ' )
2022-10-08 21:17:31 +02:00
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 ' )
2022-10-09 07:12:43 +02:00
ap . add_argument ( ' --bbox ' , type = float , nargs = 4 , metavar = ( ' LON ' , ' LAT ' , ' LON ' , ' LAT ' ) , default = None , help = ' Override map bounding box ' )
2022-10-08 21:17:31 +02:00
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 ( ' --font ' , type = str , default = ' style/concertone-regular.ttf ' , help = ' Name of the font used in the stylesheet. ' )
2022-10-09 22:40:01 +02:00
ap . add_argument ( ' --font-size ' , type = int , default = 45 , help = ' Size of the font used in the stylesheet. ' )
2022-10-12 02:43:47 +02:00
ap . add_argument ( ' --font-min-distance ' , type = int , default = 18 , help = ' Minimal distance of labels from their dots center ' )
ap . add_argument ( ' --font-max-distance ' , type = int , default = 40 , help = ' Maximal distance of labels from their dots center ' )
ap . add_argument ( ' --font-step-distance ' , type = int , default = 3 , help = ' Distance steps of labels from their dots center ' )
2022-10-08 21:17:31 +02:00
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 ' )
2022-10-09 07:12:43 +02:00
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 " ) ' )
2022-10-08 21:17:31 +02:00
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-y ' , type = float , default = 200 , help = ' Y axis scale to apply after projecting ' )
2022-10-12 03:10:17 +02:00
ap . add_argument ( ' --png-scale ' , type = float , default = 1.0 , help = ' Scale of the PNG image ' )
2022-10-08 21:17:31 +02:00
ap . add_argument ( ' --debug ' , action = ' store_true ' , default = False , help = ' Add debug information to the produced SVG ' )
ns = ap . parse_args ( sys . argv [ 1 : ] )
if ns . update_borders or not os . path . isdir ( ns . cache_directory . shapes_countries ) :
if os . path . isdir ( ns . cache_directory . shapes_countries ) :
shutil . rmtree ( ns . cache_directory . shapes_countries )
fetch_wikidata_countries ( target = ns . cache_directory . shapes_countries )
if ns . update_borders or not os . path . isdir ( ns . cache_directory . shapes_states ) :
if os . path . isdir ( ns . cache_directory . shapes_states ) :
shutil . rmtree ( ns . cache_directory . shapes_states )
fetch_wikidata_states ( target = ns . cache_directory . shapes_states )
if ns . update_erfalist or not os . path . isfile ( ns . cache_directory . erfa_info ) :
if os . path . exists ( ns . cache_directory . erfa_info ) :
os . unlink ( ns . cache_directory . erfa_info )
fetch_erfas ( target = ns . cache_directory . erfa_info , url = ERFA_URL )
if ns . update_erfalist or not os . path . isfile ( ns . cache_directory . chaostreff_info ) :
if os . path . exists ( ns . cache_directory . chaostreff_info ) :
os . unlink ( ns . cache_directory . chaostreff_info )
fetch_erfas ( target = ns . cache_directory . chaostreff_info , url = CHAOSTREFF_URL )
bbox = compute_bbox ( ns )
filter_boundingbox ( ns . cache_directory . shapes_countries , ns . cache_directory . shapes_filtered , bbox )
create_svg ( ns , bbox )
if __name__ == ' __main__ ' :
main ( )