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
2022-10-13 03:20:34 +02:00
import abc
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-27 01:21:38 +02:00
from geopy . extra . rate_limiter import RateLimiter
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 . 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
2022-10-14 21:49:16 +02:00
self . svg_path = os . path . join ( path , ' map.svg ' )
2022-10-14 20:32:58 +02:00
self . font_path = os . path . join ( path , ' style/font.ttf ' )
self . css_path = os . path . join ( path , ' style/erfamap.css ' )
2022-10-12 02:43:47 +02:00
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
2022-10-13 03:20:34 +02:00
class CoordinateTransform :
2022-10-09 01:15:36 +02:00
2022-10-13 03:20:34 +02:00
def __init__ ( self , projection : str ) :
self . _proj = pyproj . Transformer . from_crs ( ' epsg:4326 ' , projection )
self . setup ( )
2022-10-08 21:17:31 +02:00
2022-10-13 03:20:34 +02:00
def setup ( self , scalex = 1.0 , scaley = 1.0 , bbox = [ ( - 180 , - 90 ) , ( 180 , 90 ) ] ) :
self . _scalex = scalex
self . _scaley = scaley
west = min ( bbox [ 0 ] [ 0 ] , bbox [ 1 ] [ 0 ] )
north = max ( bbox [ 0 ] [ 1 ] , bbox [ 1 ] [ 1 ] )
east = max ( bbox [ 0 ] [ 0 ] , bbox [ 1 ] [ 0 ] )
south = min ( bbox [ 0 ] [ 1 ] , bbox [ 1 ] [ 1 ] )
self . _ox , self . _oy = self . _proj . transform ( west , north )
return self ( east , south )
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
def __call__ ( self , lon , lat ) :
xt , yt = self . _proj . transform ( lon , lat )
return (
( xt - self . _ox ) * self . _scalex ,
( self . _oy - yt ) * self . _scaley
)
class Drawable ( abc . ABC ) :
def render ( self , svg , texts ) :
pass
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
class Country ( Drawable ) :
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
SVG_CLASS = ' country '
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
2023-06-01 04:23:45 +02:00
? item wdt : P361 + | ( wdt : P706 / wdt : P361 ) ? euroclass .
2022-10-09 01:15:36 +02:00
# ?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-13 03:20:34 +02:00
'''
2022-10-14 21:49:16 +02:00
def __init__ ( self , ns , name , polygon ) :
2022-10-13 03:20:34 +02:00
self . name = name
2022-10-14 21:49:16 +02:00
self . ns = ns
self . orig_polygon = polygon [ 0 ]
self . _proj_polygon = None
@property
def polygon ( self ) :
if self . _proj_polygon is None :
self . _proj_polygon = [ self . ns . projection ( lon , lat ) for lon , lat in self . orig_polygon ]
return self . _proj_polygon
2022-10-13 03:20:34 +02:00
def __len__ ( self ) :
2022-10-14 21:49:16 +02:00
return len ( self . polygon )
def filter_boundingbox ( self , bbox ) :
for point in self . orig_polygon :
if point [ 0 ] > = bbox [ 0 ] [ 0 ] and point [ 1 ] > = bbox [ 0 ] [ 1 ] \
and point [ 0 ] < = bbox [ 1 ] [ 0 ] and point [ 1 ] < = bbox [ 1 ] [ 1 ] :
return True
return False
2022-10-13 03:20:34 +02:00
def render ( self , svg , texts ) :
2022-10-14 21:49:16 +02:00
points = ' ' . join ( [ f ' { x } , { y } ' for x , y in self . polygon ] )
poly = etree . Element ( ' polygon ' , points = points )
poly . set ( ' class ' , self . __class__ . SVG_CLASS )
poly . set ( ' data-country ' , self . name )
svg . append ( poly )
2022-10-13 03:20:34 +02:00
@classmethod
def sparql_query ( cls ) :
headers = {
' Content-Type ' : ' application/x-www-form-urlencoded ' ,
' User-Agent ' : USER_AGENT ,
' Accept ' : ' application/sparql-results+json ' ,
}
body = urllib . parse . urlencode ( { ' query ' : cls . SPARQL_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
@classmethod
def fetch ( cls , target ) :
os . makedirs ( target , exist_ok = True )
shape_urls = cls . sparql_query ( )
candidates = { }
keep = { }
for item , url in tqdm . tqdm ( shape_urls . items ( ) ) :
try :
with urllib . request . urlopen ( url ) as resp :
shape = json . load ( resp )
if not shape . get ( ' license ' , ' proprietary ' ) . startswith ( ' CC0- ' ) :
# Only include public domain data
continue
candidates . setdefault ( item , [ ] ) . append ( shape )
except urllib . error . HTTPError as e :
print ( e )
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 ( ) :
2022-10-14 21:49:16 +02:00
name = shape [ ' description ' ] [ ' en ' ]
geo = shape [ ' data ' ] [ ' features ' ] [ 0 ] [ ' geometry ' ]
if geo [ ' type ' ] == ' Polygon ' :
polygons = [ geo [ ' coordinates ' ] ]
else :
polygons = geo [ ' coordinates ' ]
for i , polygon in enumerate ( polygons ) :
with open ( os . path . join ( target , f ' { item } . { i } .json ' ) , ' w ' ) as f :
json . dump ( { ' name ' : name , ' i ' : i , ' polygon ' : polygon } , f )
2022-10-13 03:20:34 +02:00
@classmethod
def from_cache ( cls , ns , source ) :
countries = [ ]
files = os . listdir ( source )
for f in files :
2022-10-14 21:49:16 +02:00
# Exclude Germany
if not f . endswith ( ' .json ' ) or f . startswith ( ' Q183. ' ) :
2022-10-13 03:20:34 +02:00
continue
path = os . path . join ( source , f )
with open ( path , ' r ' ) as sf :
2022-10-14 21:49:16 +02:00
shape = json . load ( sf )
name = shape [ ' name ' ]
polygon = shape [ ' polygon ' ]
countries . append ( cls ( ns , name , polygon ) )
2022-10-13 03:20:34 +02:00
return countries
class FederalState ( Country ) :
SVG_CLASS = ' state '
SPARQL_QUERY = '''
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
}
'''
class Erfa ( Drawable ) :
SMW_URL = ' https://doku.ccc.de/index.php?title=Spezial:Semantische_Suche&x=-5B-5BKategorie % 3A {category} -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= {limit} &link=all&headers=show&searchlabel=JSON&class=sortable+wikitable+smwtable&sort=&order=asc&offset= {offset} &mainlabel=&prettyprint=true&unescape=true '
SMW_REQUEST_LIMIT = 50
SMW_CATEGORY = ' Erfa-2DKreise '
SVG_CLASS = ' erfa '
SVG_DATA = ' data-erfa '
SVG_LABEL = True
SVG_LABELCLASS = ' erfalabel '
def __init__ ( self , ns , name , city , lon , lat , display_name = None , web = None , radius = 15 ) :
self . name = name
self . city = city
self . lon = lon
self . lat = lat
self . x , self . y = ns . projection ( lon , lat )
self . display_name = display_name if display_name is not None else name
2022-10-14 20:32:58 +02:00
if city . lower ( ) not in self . display_name . lower ( ) :
self . display_name + = f ' ( { city } ) '
2022-10-13 03:20:34 +02:00
self . web = web
self . radius = radius
def __eq__ ( self , o ) :
if not isinstance ( o , Erfa ) :
return False
return self . name == o . name
def render ( self , svg , texts ) :
cls = self . __class__
if self . web is not None :
group = etree . Element ( ' a ' , href = self . web , target = ' _blank ' )
else :
group = etree . Element ( ' g ' )
group . set ( cls . SVG_DATA , self . city )
circle = etree . Element ( ' circle ' , cx = str ( self . x ) , cy = str ( self . y ) , r = str ( self . radius ) )
circle . set ( ' class ' , cls . SVG_CLASS )
circle . set ( cls . SVG_DATA , self . city )
title = etree . Element ( ' title ' )
title . text = self . display_name
circle . append ( title )
group . append ( circle )
if self . city in texts :
box = texts [ self . city ]
text = etree . Element ( ' text ' , x = str ( box . left ) , y = str ( box . top + box . meta [ ' baseline ' ] ) )
text . set ( ' class ' , cls . SVG_LABELCLASS )
text . set ( cls . SVG_DATA , self . city )
text . text = box . meta [ ' text ' ]
group . append ( text )
svg . append ( group )
@classmethod
def address_lookup ( cls , attr ) :
2022-10-27 01:21:38 +02:00
# Nominatim's Usage Policy requires rate limiting to 1 request per seconds
nominatim = Nominatim ( user_agent = USER_AGENT )
geocode = RateLimiter ( nominatim . geocode , min_delay_seconds = 1 )
2022-10-13 03:20:34 +02:00
number = attr [ ' Chaostreff-Physical-Housenumber ' ]
street = attr [ ' Chaostreff-Physical-Address ' ]
zipcode = attr [ ' Chaostreff-Physical-Postcode ' ]
acity = attr [ ' Chaostreff-Physical-City ' ]
city = attr [ ' Chaostreff-City ' ] [ 0 ]
country = attr [ ' Chaostreff-Country ' ] [ 0 ]
# Try the most accurate address first, try increasingly inaccurate addresses on failure.
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 } ' )
for fmt in formats :
2022-10-27 01:21:38 +02:00
response = geocode ( fmt )
2022-10-13 03:20:34 +02:00
if response is not None :
return response . longitude , response . latitude
print ( f ' No location found for { name } , tried the following address formats: ' )
for fmt in formats :
print ( f ' { fmt } ' )
return None
@classmethod
2022-10-14 20:32:58 +02:00
def from_api ( cls , ns , name , attr , radius ) :
2022-10-13 03:20:34 +02:00
city = attr [ ' Chaostreff-City ' ] [ 0 ]
location = cls . address_lookup ( attr )
2022-10-08 04:24:14 +02:00
if location is None :
2022-10-13 03:20:34 +02:00
raise ValueError ( f ' No location for { name } ' )
lon , lat = location
# There are up to 4 different names: 3 SMW attrs and the page name
if len ( attr [ ' Chaostreff-Longname ' ] ) > 0 :
display_name = attr [ ' Chaostreff-Longname ' ] [ 0 ]
elif len ( attr [ ' Chaostreff-Nickname ' ] ) > 0 :
display_name = attr [ ' Chaostreff-Nickname ' ] [ 0 ]
elif len ( attr [ ' Chaostreff-Realname ' ] ) > 0 :
display_name = attr [ ' Chaostreff-Realname ' ] [ 0 ]
2022-10-12 02:43:47 +02:00
else :
2022-10-13 03:20:34 +02:00
display_name = name
if len ( attr [ ' Public-Web ' ] ) > 0 :
web = attr [ ' Public-Web ' ] [ 0 ]
else :
web = None
return cls ( ns , name , city , lon , lat , display_name , web , radius )
def to_dict ( self ) :
return {
' name ' : self . name ,
' city ' : self . city ,
' web ' : self . web ,
' display_name ' : self . display_name ,
' location ' : [ self . lon , self . lat ]
}
@classmethod
def from_dict ( cls , ns , dct , radius ) :
name = dct [ ' name ' ]
city = dct [ ' city ' ]
lon , lat = dct [ ' location ' ]
display_name = dct . get ( ' display_name ' , name )
web = dct . get ( ' web ' )
radius = radius
return Erfa ( ns , name , city , lon , lat , display_name , web , radius )
@classmethod
def fetch ( cls , ns , target , radius ) :
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 = [ ]
offset = 0
while True :
url = cls . SMW_URL . format (
category = cls . SMW_CATEGORY ,
limit = cls . SMW_REQUEST_LIMIT ,
offset = offset
)
req = urllib . request . Request ( url , headers = { ' Authorization ' : f ' Basic { auth } ' } )
with urllib . request . urlopen ( req ) as resp :
body = resp . read ( ) . decode ( )
if len ( body ) == 0 :
# All pages read, new response is empty
break
erfadata = json . loads ( body )
if erfadata [ ' rows ' ] == 0 :
break
offset + = erfadata [ ' rows ' ]
for name , attr in erfadata [ ' results ' ] . items ( ) :
try :
erfa = cls . from_api ( ns , name , attr [ ' printouts ' ] , radius )
erfas . append ( erfa )
except BaseException as e :
print ( e )
continue
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
# Save to cache
with open ( target , ' w ' ) as f :
json . dump ( [ e . to_dict ( ) for e in erfas ] , f )
return erfas
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
@classmethod
def from_cache ( cls , ns , source , radius ) :
with open ( source , ' r ' ) as f :
data = json . load ( f )
return [ Erfa . from_dict ( ns , d , radius ) for d in data ]
2022-10-08 21:17:31 +02:00
2022-10-13 03:20:34 +02:00
class Chaostreff ( Erfa ) :
SMW_CATEGORY = ' Chaostreffs '
SVG_CLASS = ' chaostreff '
SVG_DATA = ' data-chaostreff '
SVG_DOTSIZE_ATTR = ' dotsize_treff '
SVG_LABEL = False
2022-10-08 21:17:31 +02:00
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 )
2022-10-12 03:37:23 +02:00
maxs = [ ]
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 :
2022-10-13 03:20:34 +02:00
if o . meta [ ' erfa ' ] == self . meta [ ' erfa ' ] :
2022-10-09 07:12:43 +02:00
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 03:37:23 +02:00
maxs . append ( max ( pdist * 2 - swm . chebyshev_distance ( o ) , 0 ) )
2022-10-13 03:20:34 +02:00
for erfa in erfas :
if erfa == self . meta [ ' erfa ' ] :
2022-10-09 07:12:43 +02:00
continue
2022-10-13 03:20:34 +02:00
location = ( erfa . x , erfa . y )
2022-10-09 07:12:43 +02:00
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-13 03:20:34 +02:00
maxs . append ( max ( pdist * 2 - swe . chebyshev_distance ( location ) , 0 ) )
for treff in chaostreffs :
if treff == self . meta [ ' erfa ' ] :
2022-10-09 07:12:43 +02:00
continue
2022-10-13 03:20:34 +02:00
location = ( treff . x , treff . y )
2022-10-09 07:12:43 +02:00
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-12 03:37:23 +02:00
maxs . append ( max ( pdist * 2 - swc . chebyshev_distance ( location ) , 0 ) )
if len ( maxs ) > 0 :
w + = max ( maxs )
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
2022-10-13 03:20:34 +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 ] ) :
erfas = Erfa . from_cache ( ns , path , radius = 0 )
for e in erfas :
if len ( bounds ) == 0 :
bounds . append ( e . lon )
bounds . append ( e . lat )
bounds . append ( e . lon )
bounds . append ( e . lat )
else :
bounds [ 0 ] = min ( bounds [ 0 ] , e . lon )
bounds [ 1 ] = min ( bounds [ 1 ] , e . lat )
bounds [ 2 ] = max ( bounds [ 2 ] , e . lon )
bounds [ 3 ] = max ( bounds [ 3 ] , e . lat )
return [
( bounds [ 0 ] - ns . bbox_margin , bounds [ 1 ] - ns . bbox_margin ) ,
( bounds [ 2 ] + ns . bbox_margin , bounds [ 3 ] + ns . bbox_margin )
]
2022-10-14 20:32:58 +02:00
def optimize_text_layout ( ns , font , erfas , chaostreffs , width , svg ) :
2022-10-12 02:43:47 +02:00
2022-10-14 20:32:58 +02:00
# Measure the various different font heights
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-13 03:20:34 +02:00
for erfa in erfas :
city = erfa . city
text = erfa . city
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
2022-10-13 03:20:34 +02:00
meta = { ' erfa ' : erfa , ' text ' : text , ' baseline ' : capheight }
2022-10-09 22:40:01 +02:00
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
2022-10-13 03:20:34 +02:00
if erfa . x > 0.8 * width :
2022-10-12 02:43:47 +02:00
bwr = bw + 0.001
else :
bwl = bw + 0.001
2022-10-13 03:20:34 +02:00
candidates [ city ] . append ( BoundingBox ( erfa . x - dist - mw , erfa . y + voffset + ns . dotsize_erfa * i * 2 , width = mw , height = mh , meta = meta , base_weight = bwl ) )
candidates [ city ] . append ( BoundingBox ( erfa . x + dist , erfa . y + voffset + ns . dotsize_erfa * i * 2 , width = mw , height = mh , meta = meta , base_weight = bwr ) )
2022-10-12 02:43:47 +02:00
# Generate 3 candidates each above and beneath the dot, aligned left, centered and right
candidates [ city ] . extend ( [
2022-10-13 03:20:34 +02:00
BoundingBox ( erfa . x - mw / 2 , erfa . y - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
BoundingBox ( erfa . x - mw / 2 , erfa . y + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.001 ) ,
BoundingBox ( erfa . x - ns . dotsize_erfa , erfa . y - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.002 ) ,
BoundingBox ( erfa . x - ns . dotsize_erfa , erfa . y + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.003 ) ,
BoundingBox ( erfa . x + ns . dotsize_erfa - mw , erfa . y - dist - mh , width = mw , height = mh , meta = meta , base_weight = bw + 0.002 ) ,
BoundingBox ( erfa . x + ns . dotsize_erfa - mw , erfa . y + dist , width = mw , height = mh , meta = meta , base_weight = bw + 0.003 ) ,
2022-10-12 02:43:47 +02:00
] )
# 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
2022-10-13 03:20:34 +02:00
unfinished = { e . city for e in erfas }
2022-10-08 21:17:31 +02:00
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 )
2022-10-13 03:20:34 +02:00
mincity = minbox . meta [ ' erfa ' ] . city
2022-10-09 22:40:01 +02:00
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-13 03:20:34 +02:00
def create_imagemap ( ns , size , parent , erfas , chaostreffs , texts ) :
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 ' )
2022-10-13 03:20:34 +02:00
for erfa in erfas :
if erfa . web is None :
2022-10-12 02:43:47 +02:00
continue
area = etree . Element ( ' area ' ,
shape = ' circle ' ,
2022-10-13 03:20:34 +02:00
coords = f ' { erfa . x * s } , { erfa . y * s } , { erfa . radius * s } ' ,
href = erfa . web ,
title = erfa . display_name )
2022-10-12 02:43:47 +02:00
imgmap . append ( area )
2022-10-13 03:20:34 +02:00
if erfa . city in texts :
box = texts [ erfa . city ]
area2 = etree . Element ( ' area ' ,
shape = ' rect ' ,
coords = f ' { box . left * s } , { box . top * s } , { box . right * s } , { box . bottom * s } ' ,
href = erfa . web ,
title = erfa . display_name )
imgmap . append ( area2 )
2022-10-12 02:43:47 +02:00
parent . append ( img )
parent . append ( imgmap )
2022-10-14 21:49:16 +02:00
def create_svg ( ns ) :
2022-10-08 21:17:31 +02:00
print ( ' Creating SVG image ' )
2022-10-14 20:32:58 +02:00
# Load the font used for the labels
font = ImageFont . truetype ( ns . font , ns . font_size )
2022-10-08 21:17:31 +02:00
# Convert from WGS84 lon, lat to chosen projection
2022-10-14 21:49:16 +02:00
bbox = compute_bbox ( ns )
2022-10-13 03:20:34 +02:00
svg_box = ns . projection . setup ( ns . scale_x , ns . scale_y , bbox = bbox )
2022-10-08 04:24:14 +02:00
rectbox = [ 0 , 0 , svg_box [ 0 ] , svg_box [ 1 ] ]
2022-10-13 03:20:34 +02:00
# Load everything from cached JSON files
2022-10-14 21:49:16 +02:00
countries = Country . from_cache ( ns , ns . cache_directory . shapes_countries )
countries = [ c for c in countries if c . filter_boundingbox ( bbox ) ]
2022-10-13 03:20:34 +02:00
states = FederalState . from_cache ( ns , ns . cache_directory . shapes_states )
erfas = Erfa . from_cache ( ns , ns . cache_directory . erfa_info , ns . dotsize_erfa )
chaostreffs = Chaostreff . from_cache ( ns , ns . cache_directory . chaostreff_info , ns . dotsize_treff )
# 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.
chaostreffs = [ c for c in chaostreffs if c not in erfas ]
for c in states + countries :
2022-10-14 21:49:16 +02:00
for x , y in c . polygon :
rectbox [ 0 ] = min ( x , rectbox [ 0 ] )
rectbox [ 1 ] = min ( y , rectbox [ 1 ] )
rectbox [ 2 ] = max ( x , rectbox [ 2 ] )
rectbox [ 3 ] = max ( y , rectbox [ 3 ] )
2022-10-08 04:24:14 +02:00
2022-10-12 02:43:47 +02:00
print ( ' Copying stylesheet and font ' )
2022-10-14 20:32:58 +02:00
os . makedirs ( os . path . dirname ( ns . output_directory . font_path ) , exist_ok = True )
shutil . copyfile ( ns . font , ns . output_directory . font_path )
os . makedirs ( os . path . dirname ( ns . output_directory . css_path ) , exist_ok = True )
fontrelpath = os . path . relpath ( ns . output_directory . font_path ,
start = os . path . dirname ( ns . output_directory . css_path ) )
with open ( ns . stylesheet , ' r ' ) as src :
with open ( ns . output_directory . css_path , ' w ' ) as dst :
dst . write ( f '''
@font - face { {
font - family : " {font.font.family} " ;
src : url ( " {fontrelpath} " ) ;
} }
''' )
dst . write ( src . read ( ) )
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-14 21:49:16 +02:00
style = etree . Element ( ' style ' , type = ' text/css ' )
with open ( ns . stylesheet , ' r ' ) as css :
style . text = css . read ( )
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
# This can take some time, especially if lots of candidates are generated
print ( ' Layouting labels ' )
2022-10-14 20:32:58 +02:00
texts = optimize_text_layout ( ns , font , erfas , chaostreffs , width = svg_box [ 0 ] , svg = svg )
2022-10-12 02:43:47 +02:00
2022-10-13 03:20:34 +02:00
# Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
states = sorted ( states , key = lambda x : - len ( x ) )
# Render country and state borders
for c in countries + states :
c . render ( svg , texts )
2022-10-08 04:24:14 +02:00
2022-10-13 03:20:34 +02:00
# Render Erfa dots and their labels
for erfa in erfas + chaostreffs :
erfa . render ( svg , texts )
2022-10-08 21:17:31 +02:00
2022-10-12 02:43:47 +02:00
# Generate SVG, PNG and HTML output files
2022-10-14 21:49:16 +02:00
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-14 20:32:58 +02:00
2022-10-08 21:17:31 +02:00
2022-10-12 02:43:47 +02:00
print ( ' Writing PNG ' )
2022-10-14 21:49:16 +02:00
cairosvg . svg2png ( url = ns . output_directory . svg_path , write_to = ns . output_directory . png_path ,
2022-10-12 03:10:17 +02:00
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 ' )
2022-10-14 20:32:58 +02:00
link = etree . Element ( ' link ' , rel = ' stylesheet ' , href = ns . output_directory . rel ( ns . output_directory . css_path ) )
2022-10-12 02:43:47 +02:00
head . append ( link )
html . append ( head )
body = etree . Element ( ' body ' )
obj = etree . Element ( ' object ' ,
2022-10-14 21:49:16 +02:00
data = ns . output_directory . rel ( ns . output_directory . svg_path ) ,
2022-10-12 02:43:47 +02:00
width = str ( svg_box [ 0 ] ) , height = str ( svg_box [ 1 ] ) )
2022-10-13 03:20:34 +02:00
create_imagemap ( ns , svg_box , obj , erfas , chaostreffs , texts )
2022-10-12 02:43:47 +02:00
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 ' )
2022-10-14 20:32:58 +02:00
link = etree . Element ( ' link ' , rel = ' stylesheet ' , href = ns . output_directory . rel ( ns . output_directory . css_path ) )
2022-10-12 03:10:17 +02:00
head . append ( link )
html . append ( head )
2022-10-12 02:43:47 +02:00
body = etree . Element ( ' body ' )
html . append ( body )
2022-10-13 03:20:34 +02:00
create_imagemap ( ns , svg_box , body , erfas , chaostreffs , texts )
2022-10-12 02:43:47 +02:00
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-13 03:20:34 +02:00
ap . add_argument ( ' --projection ' , type = CoordinateTransform , default = ' epsg:4258 ' , help = ' Map projection to convert the WGS84 coordinates to ' )
2022-10-08 21:17:31 +02:00
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 )
2022-10-13 03:20:34 +02:00
print ( ' Retrieving country border shapes ' )
Country . fetch ( target = ns . cache_directory . shapes_countries )
2022-10-08 21:17:31 +02:00
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 )
2022-10-13 03:20:34 +02:00
print ( ' Retrieving state border shapes ' )
FederalState . fetch ( target = ns . cache_directory . shapes_states )
2022-10-08 21:17:31 +02:00
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 )
2022-10-13 03:20:34 +02:00
print ( ' Retrieving Erfas information ' )
Erfa . fetch ( ns , target = ns . cache_directory . erfa_info , radius = ns . dotsize_erfa )
2022-10-08 21:17:31 +02:00
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 )
2022-10-13 03:20:34 +02:00
print ( ' Retrieving Chaostreffs information ' )
2022-10-14 20:32:58 +02:00
Chaostreff . fetch ( ns , target = ns . cache_directory . chaostreff_info , radius = ns . dotsize_treff )
2022-10-08 21:17:31 +02:00
2022-10-14 21:49:16 +02:00
create_svg ( ns )
2022-10-08 21:17:31 +02:00
if __name__ == ' __main__ ' :
main ( )