erfamap/generate_map.py

285 lines
11 KiB
Python
Raw Normal View History

2022-10-08 04:24:14 +02:00
#!/usr/bin/env python3
import os
import urllib.request
import json
import base64
import pyproj
from SPARQLWrapper import SPARQLWrapper, JSON
from geopy import Nominatim
BOUNDING_BOX = [(4.27775, 46.7482713), (19.2403594, 54.9833021)]
ERFA_URL = 'https://doku.ccc.de/Spezial:Semantische_Suche/format%3Djson/limit%3D50/link%3Dall/headers%3Dshow/searchlabel%3DJSON/class%3Dsortable-20wikitable-20smwtable/sort%3D/order%3Dasc/offset%3D0/-5B-5BKategorie:Erfa-2DKreise-5D-5D-20-5B-5BChaostreff-2DActive::wahr-5D-5D/-3FChaostreff-2DCity/-3FChaostreff-2DPhysical-2DAddress/-3FChaostreff-2DPhysical-2DHousenumber/-3FChaostreff-2DPhysical-2DPostcode/-3FChaostreff-2DPhysical-2DCity/-3FChaostreff-2DCountry/mainlabel%3D/prettyprint%3Dtrue/unescape%3Dtrue'
CHAOSTREFF_URL = 'https://doku.ccc.de/Spezial:Semantische_Suche/format%3Djson/limit%3D50/link%3Dall/headers%3Dshow/searchlabel%3DJSON/class%3Dsortable-20wikitable-20smwtable/sort%3D/order%3Dasc/offset%3D0/-5B-5BKategorie:Chaostreffs-5D-5D-20-5B-5BChaostreff-2DActive::wahr-5D-5D/-3FChaostreff-2DCity/-3FChaostreff-2DPhysical-2DAddress/-3FChaostreff-2DPhysical-2DHousenumber/-3FChaostreff-2DPhysical-2DPostcode/-3FChaostreff-2DPhysical-2DCity/-3FChaostreff-2DCountry/mainlabel%3D/prettyprint%3Dtrue/unescape%3Dtrue'
def fetch_wikidata_states(target='shapes_states'):
sparql = SPARQLWrapper('https://query.wikidata.org/sparql')
sparql.setQuery('''
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
}
''')
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
shape_urls = {result['item']['value'].split('/')[-1]: result['map']['value'] for result in results["results"]["bindings"]}
os.makedirs(target, exist_ok=True)
for item, url in shape_urls.items():
with urllib.request.urlopen(url) as resp:
with open(os.path.join(target, item + '.json'), 'wb') as f:
f.write(resp.read())
def fetch_wikidata_countries(target='shapes_countries'):
sparql = SPARQLWrapper('https://query.wikidata.org/sparql')
sparql.setQuery('''
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
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
?item wdt:P361+ ?euroclass;
# ?item has geoshape ?map
wdt:P3896 ?map.
FILTER (?stateclass = wd:Q6256 || ?stateclass = wd:Q3624078).
FILTER (?euroclass = wd:Q46 || ?euroclass = wd:Q8932).
}
''')
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
shape_urls = {result['item']['value'].split('/')[-1]: result['map']['value'] for result in results["results"]["bindings"]}
os.makedirs(target, exist_ok=True)
for item, url in shape_urls.items():
try:
with urllib.request.urlopen(url) as resp:
with open(os.path.join(target, item + '.json'), 'wb') as f:
f.write(resp.read())
except BaseException as e:
print(e)
print(url)
def filter_boundingbox(source='shapes_countries', target='shapes_filtered'):
files = os.listdir(source)
os.makedirs(target, exist_ok=True)
for f in files:
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]:
if point[0] >= BOUNDING_BOX[0][0] and point[1] >= BOUNDING_BOX[0][1] \
and point[0] <= BOUNDING_BOX[1][0] and point[1] <= BOUNDING_BOX[1][1]:
keep = True
break
if keep:
break
if keep:
with open(os.path.join(target, f), 'w') as sf:
sf.write(shapedata)
def address_lookup(erfa):
locator = Nominatim(user_agent='foobar')
city = erfa['printouts']['Chaostreff-City'][0]
country = erfa['printouts']['Chaostreff-Country'][0]
address = f'{city}, {country}'
response = locator.geocode(address)
if response is None:
return None
return response.longitude, response.latitude
def fetch_erfas(target='erfa-info.json', url=ERFA_URL, bbox=None):
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())
for name, erfa in erfadata['results'].items():
location = address_lookup(erfa)
if location is None:
print(f'WARNING: No location for {name}')
city = erfa['printouts']['Chaostreff-City'][0]
erfas[city] = location
if len(bbox) == 0:
bbox.append(location)
bbox.append(location)
else:
bbox[0] = (min(bbox[0][0], location[0]), min(bbox[0][1], location[1]))
bbox[1] = (max(bbox[1][0], location[0]), max(bbox[1][1], location[1]))
with open(target, 'w') as f:
json.dump(erfas, f)
def fetch_chaostreffs(target='chaostreff-info.json', bbox=None):
fetch_erfas(target=target, url=CHAOSTREFF_URL, bbox=bbox)
def create_svg(source_states='shapes_states', source_countries='shapes_filtered', source_erfa='erfa-info.json', source_ct='chaostreff-info.json'):
transformer = pyproj.Transformer.from_crs('epsg:4326', 'epsg:4258')
scalex = 130
scaley = 200
blt = transformer.transform(*BOUNDING_BOX[0])
trt = transformer.transform(*BOUNDING_BOX[1])
jtm_bounding_box = [
(scalex*blt[0], scaley*trt[1]),
(scalex*trt[0], scaley*blt[1])
]
origin = jtm_bounding_box[0]
svg_box = (jtm_bounding_box[1][0] - origin[0], origin[1] - jtm_bounding_box[1][1])
shapes_states = []
files = os.listdir(source_states)
for f in files:
if not f.endswith('.json'):
continue
path = os.path.join(source_states, f)
with open(path, 'r') as sf:
shapedata = sf.read()
shape = json.loads(shapedata)
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))
shapes_states.append(ts)
shapes_countries = []
files = os.listdir(source_countries)
for f in files:
if not f.endswith('.json'):
continue
path = os.path.join(source_countries, f)
with open(path, 'r') as sf:
shapedata = sf.read()
shape = json.loads(shapedata)
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))
shapes_countries.append(ts)
chaostreffs = {}
with open(source_ct, 'r') as f:
ctdata = json.load(f)
for city, location in ctdata.items():
if location is None:
continue
xt, yt = transformer.transform(*location)
chaostreffs[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
erfas = {}
with open(source_erfa, 'r') as f:
ctdata = json.load(f)
for city, location in ctdata.items():
if location is None:
continue
xt, yt = transformer.transform(*location)
erfas[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
rectbox = [0, 0, svg_box[0], svg_box[1]]
for shape in shapes_states + shapes_countries:
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])
SVG = f'''
<svg viewBox="0 0 {svg_box[0]} {svg_box[1]}" width="{svg_box[0]}" height="{svg_box[1]}"
xmlns="http://www.w3.org/2000/svg">
<rect x="{rectbox[0]}" y="{rectbox[1]}" width="{rectbox[3]-rectbox[1]}" height="{rectbox[2]-rectbox[0]}" stroke="none" fill="#759eb5" />
'''
# 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)):
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
SVG += f'''
<polygon points="{points}" fill="#ffffff" stroke="#c3c3c3" stroke-width="3" />
'''
for shape in shapes_countries:
points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
SVG += f'''
<polygon points="{points}" fill="#c3c3c3" stroke="#ffffff" stroke-width="3" />
'''
for city, location in erfas.items():
SVG += f'''
<circle cx="{location[0]}" cy="{location[1]}" r="15" fill="#f47e1e" stroke="#ffffff" stroke-width="3" />
<!--<rect x="{location[0]+25}" y="{location[1]-15}" width="{len(city)*15}" height="30" stroke="green" stroke-width="1" fill="none" />
<rect x="{location[0]-25-len(city)*15}" y="{location[1]-15}" width="{len(city)*15}" height="30" stroke="red" stroke-width="1" fill="none" />-->
'''
for city, location in chaostreffs.items():
SVG += f'''
<circle cx="{location[0]}" cy="{location[1]}" r="8" fill="#f47e1e" stroke="#ffffff" stroke-width="3" />
'''
for city, location in erfas.items():
weight_right = sum([1 for x, y in erfas.values() if x > location[0] + 25 and x < location[0] + 25 + len(city)*15 and y > location[1] - 15 and y < location[1] + 25])
weight_left = sum([1 for x, y in erfas.values() if x < location[0] - 25 and x > location[0] - 25 - len(city)*15 and y > location[1] - 15 and y < location[1] + 25])
if weight_right > weight_left:
SVG += f'''
<text x="{location[0]-25}" y="{location[1]+7.5}" text-anchor="end" style="font-family: Liberation Sans; font-size: 25; font-weight: bold;">{city}</text>
'''
else:
SVG += f'''
<text x="{location[0]+25}" y="{location[1]+7.5}" style="font-family: Liberation Sans; font-size: 25; font-weight: bold;">{city}</text>
'''
SVG += '</svg>'
with open('map.svg', 'w') as mapfile:
mapfile.write(SVG)
bbox = []
fetch_erfas(bbox=bbox)
#fetch_chaostreffs(bbox=bbox)
#print(bbox)
#fetch_wikidata_states()
#fetch_wikidata_countries()
filter_boundingbox()
create_svg()
# Q347 P361 Q46