diff --git a/cache.example/chaostreff-info.json b/cache.example/chaostreff-info.json
index 9783c01..eb4b700 100644
--- a/cache.example/chaostreff-info.json
+++ b/cache.example/chaostreff-info.json
@@ -1 +1 @@
-{"Regensburg": {"location": [12.11902809759291, 49.009833549999996], "web": "https://binary-kitchen.de", "name": "Binary Kitchen"}, "Erfurt": {"location": [11.039392, 50.9828132], "web": "https://bytespeicher.org/", "name": "Bytespeicher"}, "Amsterdam": {"location": [4.870297612903226, 52.36339370322581], "web": "https://chaos.amsterdam/", "name": "Chaos Amsterdam"}, "Luxemburg": {"location": [6.1226317, 49.6207341], "web": "https://c3l.lu/", "name": "Chaos Computer Club L\u00ebtzebuerg"}, "Kiel": {"location": [10.135555, 54.3227085], "web": "https://toppoint.de/", "name": "Chaostreff in Kiel"}, "Alzey": {"location": [8.328899881948397, 49.702704049999994], "web": "http://chaostreff-alzey.de/", "name": "Chaostreff Alzey"}, "Augsburg": {"location": [10.886817639154547, 48.35789355], "web": "http://c3a.de", "name": "Chaostreff Augsburg"}, "Backnang": {"location": [9.4295622, 48.9487308], "web": "https://hacknang.de/", "name": "Digitales Backnang"}, "Bayreuth": {"location": [11.574946989405685, 49.9477097], "web": "https://imaginaerraum.de/", "name": "Hackerspace Bayreuth"}, "Bern": {"location": [7.4484931, 46.9564373], "web": "https://www.chaostreffbern.ch", "name": "Chaostreff Bern"}, "Bielefeld": {"location": [8.533127303708476, 52.038277], "web": "https://hackerspace-bielefeld.de/", "name": "Chaos Computer Club Bielefeld"}, "Budapest": {"location": [19.059225604898185, 47.489209200000005], "web": "https://hsbp.org/", "name": "Chaostreff Budapest"}, "Chemnitz": {"location": [12.9298883, 50.8166968], "web": "https://chaoschemnitz.de/", "name": "Chaostreff Chemnitz"}, "Coburg": {"location": [10.9660664, 50.2633598], "web": "https://hackzogtum-coburg.de/", "name": "Hackzogtum Coburg"}, "Rapperswil-Jona": {"location": [8.83370045, 47.2252042], "web": "https://www.coredump.ch/", "name": "Chaostreff Coredump (Rapperswil)"}, "Cottbus": {"location": [14.3221859, 51.768973], "web": "http://chaos-cb.de", "name": "Chaostreff Cottbus"}, "Flensburg": {"location": [9.423505460496958, 54.80446775], "web": "https://chaostreff-flensburg.de/", "name": "Chaostreff Flensburg e.V."}, "Gie\u00dfen": {"location": [8.6790748, 50.5820278], "web": "https://giessen.ccc.de", "name": "Chaostreff Gie\u00dfen"}, "Graz": {"location": [15.450608284226139, 47.0655229], "web": "https://wp.realraum.at/", "name": "Chaostreff Graz"}, "Halle (Saale)": {"location": [11.992221563581825, 51.47991935], "web": "https://eigenbaukombinat.de/", "name": "Chaostreff Halle"}, "Heidelberg": {"location": [8.6660724, 49.4183048], "web": "https://www.noname-ev.de/", "name": "Chaostreff Heidelberg"}, "Hildesheim": {"location": [9.9467753, 52.1685013], "web": "https://freieslabor.org", "name": "Chaostreff Hildesheim"}, "Hilpoltstein": {"location": [11.19428251600182, 49.1892185], "web": "https://chaos-hip.de/", "name": "Chaostreff Hilpoltstein"}, "Ingolstadt": {"location": [11.381725, 48.7710702], "web": "https://www.bytewerk.org/", "name": "bytewerk"}, "Innsbruck": {"location": [11.3962816, 47.257827], "web": "https://it-syndikat.org/", "name": "Chaostreff Innsbruck"}, "Iserlohn": {"location": [7.6979603, 51.3743032], "web": "https://www.chaos-consulting.de/", "name": "Chaos Consulting"}, "Itzehoe": {"location": [9.50196591631791, 53.9379972], "web": "https://www.cciz.de/", "name": "Computerclub Itzehoe"}, "Jena": {"location": [11.5826767, 50.929203], "web": "https://kraut.space/", "name": "Chaostreff Jena"}, "Klaus": {"location": [9.6460406, 47.304616], "web": "https://open-lab.at/index.php/chaostreff", "name": "Chaostreff Klaus (Vorarlberg)"}, "Ludwigsburg": {"location": [9.184755297842258, 48.8958537], "web": "https://complb.de/", "name": "Chaostreff Ludwigsburg"}, "Marburg": {"location": [8.7783888, 50.8161331], "web": "https://hsmr.cc/", "name": "Hackspace Marburg"}, "M\u00fcnster": {"location": [7.6386827, 51.9446182], "web": "http://www.warpzone.ms/", "name": "Chaostref M\u00fcnster"}, "N\u00fcrnberg": {"location": [11.081815196597816, 49.44836835], "web": "https://chaostreff-nuernberg.de/", "name": "Chaostreff N\u00fcrnberg"}, "Osnabr\u00fcck": {"location": [8.047635, 52.2719595], "web": "https://chaostreff-osnabrueck.de/", "name": "Chaostreff Osnabr\u00fcck"}, "Potsdam": {"location": [13.078557, 52.3894652], "web": "http://www.ccc-p.org/", "name": "Chaostreff Potsdam"}, "Recklinghausen": {"location": [7.1691246, 51.62435], "web": "https://www.c3re.de/", "name": "Chaostreff Recklinghausen"}, "Rothenburg ob der Tauber": {"location": [10.1779991, 49.3783145], "web": "https://fablab-rothenburg.de/", "name": "FabLab Region Rothenburg ob der Tauber"}, "Rotterdam": {"location": [4.433639012034096, 51.9099513], "web": "https://www.pixelbar.nl/", "name": "Chaostreff Rotterdam"}, "Schwerin": {"location": [11.4182505, 53.6010948], "web": "https://hacklabor.de/", "name": "Chaostreff Schwerin"}, "Villingen-Schwenningen": {"location": [8.455688210119636, 48.06480785], "web": "https://vspace.one/", "name": "Chaostreff Villingen-Schwenningen"}, "Winterthur": {"location": [8.7291498, 47.4991723], "web": "http://chaostreff.dingens.org/", "name": "Chaostreff Winterthur"}, "Wuppertal": {"location": [7.144908700674055, 51.26671315], "web": "https://chaostal.de/", "name": "Chaostal (Chaostreff Wuppertal)"}, "L\u00fcbeck": {"location": [10.6713232, 53.8687751], "web": "https://chaotikum.org/", "name": "Chaotikum"}, "Bonn": {"location": [7.0987248, 50.7387823], "web": "https://datenburg.org/", "name": "Chaostreff Bonn"}, "Saarbr\u00fccken": {"location": [7.0357391, 49.279199], "web": "https://hacksaar.de/", "name": "Chaostreff Saarbr\u00fccken"}, "Aalen": {"location": [10.0931765, 48.8362705], "web": "https://aalen.space/", "name": "Chaostreff Aalen"}, "Trier": {"location": [6.6338265, 49.7529551], "web": "https://maschinendeck.org/", "name": "Maschinendeck"}, "Aschaffenburg": {"location": [9.1358843, 49.9878536], "web": "https://schaffenburg.org/", "name": "Schaffenburg"}, "Offenburg": {"location": [7.9458244, 48.4762239], "web": "https://section77.de/"}, "L\u00f6rrach": {"location": [7.6650755, 47.6155335], "web": "https://technik.cafe/", "name": "technik.cafe"}}
\ No newline at end of file
+[{"name": "Binary Kitchen", "city": "Regensburg", "web": "https://binary-kitchen.de", "display_name": "Binary Kitchen", "location": [12.11902809759291, 49.009833549999996]}, {"name": "Bytespeicher", "city": "Erfurt", "web": "https://bytespeicher.org/", "display_name": "Bytespeicher", "location": [11.039392, 50.9828132]}, {"name": "Chaos Amsterdam", "city": "Amsterdam", "web": "https://chaos.amsterdam/", "display_name": "Chaos Amsterdam", "location": [4.870297612903226, 52.36339370322581]}, {"name": "Chaos Computer Club L\u00ebtzebuerg", "city": "Luxemburg", "web": "https://c3l.lu/", "display_name": "Chaos Computer Club L\u00ebtzebuerg", "location": [6.1226317, 49.6207341]}, {"name": "ChaosKiel", "city": "Kiel", "web": "https://toppoint.de/", "display_name": "Chaostreff in Kiel", "location": [10.135555, 54.3227085]}, {"name": "Chaostreff Alzey", "city": "Alzey", "web": "http://chaostreff-alzey.de/", "display_name": "Chaostreff Alzey", "location": [8.328899881948397, 49.702704049999994]}, {"name": "Chaostreff Augsburg", "city": "Augsburg", "web": "http://c3a.de", "display_name": "Chaostreff Augsburg", "location": [10.886817639154547, 48.35789355]}, {"name": "Chaostreff Backnang", "city": "Backnang", "web": "https://hacknang.de/", "display_name": "Digitales Backnang", "location": [9.4295622, 48.9487308]}, {"name": "Chaostreff Bayreuth", "city": "Bayreuth", "web": "https://imaginaerraum.de/", "display_name": "Hackerspace Bayreuth", "location": [11.574946989405685, 49.9477097]}, {"name": "Chaostreff Bern", "city": "Bern", "web": "https://www.chaostreffbern.ch", "display_name": "Chaostreff Bern", "location": [7.4484931, 46.9564373]}, {"name": "Chaostreff Bielefeld", "city": "Bielefeld", "web": "https://hackerspace-bielefeld.de/", "display_name": "Chaos Computer Club Bielefeld", "location": [8.533127303708476, 52.038277]}, {"name": "Chaostreff Budapest", "city": "Budapest", "web": "https://hsbp.org/", "display_name": "Chaostreff Budapest", "location": [19.059225604898185, 47.489209200000005]}, {"name": "Chaostreff Chemnitz", "city": "Chemnitz", "web": "https://chaoschemnitz.de/", "display_name": "Chaostreff Chemnitz", "location": [12.9298883, 50.8166968]}, {"name": "Chaostreff Coburg", "city": "Coburg", "web": "https://hackzogtum-coburg.de/", "display_name": "Hackzogtum Coburg", "location": [10.9660664, 50.2633598]}, {"name": "Chaostreff Coredump (Rapperswil-Jona)", "city": "Rapperswil-Jona", "web": "https://www.coredump.ch/", "display_name": "Chaostreff Coredump (Rapperswil)", "location": [8.83370045, 47.2252042]}, {"name": "Chaostreff Cottbus", "city": "Cottbus", "web": "http://chaos-cb.de", "display_name": "Chaostreff Cottbus", "location": [14.3221859, 51.768973]}, {"name": "Chaostreff Flensburg", "city": "Flensburg", "web": "https://chaostreff-flensburg.de/", "display_name": "Chaostreff Flensburg e.V.", "location": [9.423505460496958, 54.80446775]}, {"name": "Chaostreff Gie\u00dfen", "city": "Gie\u00dfen", "web": "https://giessen.ccc.de", "display_name": "Chaostreff Gie\u00dfen", "location": [8.6790748, 50.5820278]}, {"name": "Chaostreff Graz", "city": "Graz", "web": "https://wp.realraum.at/", "display_name": "Chaostreff Graz", "location": [15.450608284226139, 47.0655229]}, {"name": "Chaostreff Halle (Saale)", "city": "Halle (Saale)", "web": "https://eigenbaukombinat.de/", "display_name": "Chaostreff Halle", "location": [11.992221563581825, 51.47991935]}, {"name": "Chaostreff Heidelberg", "city": "Heidelberg", "web": "https://www.noname-ev.de/", "display_name": "Chaostreff Heidelberg", "location": [8.6660724, 49.4183048]}, {"name": "Chaostreff Hildesheim", "city": "Hildesheim", "web": "https://freieslabor.org", "display_name": "Chaostreff Hildesheim", "location": [9.9467753, 52.1685013]}, {"name": "Chaostreff Hilpoltstein", "city": "Hilpoltstein", "web": "https://chaos-hip.de/", "display_name": "Chaostreff Hilpoltstein", "location": [11.19428251600182, 49.1892185]}, {"name": "Chaostreff Ingolstadt", "city": "Ingolstadt", "web": "https://www.bytewerk.org/", "display_name": "bytewerk", "location": [11.381725, 48.7710702]}, {"name": "Chaostreff Innsbruck", "city": "Innsbruck", "web": "https://it-syndikat.org/", "display_name": "Chaostreff Innsbruck", "location": [11.3962816, 47.257827]}, {"name": "Chaostreff Iserlohn", "city": "Iserlohn", "web": "https://www.chaos-consulting.de/", "display_name": "Chaos Consulting", "location": [7.6979603, 51.3743032]}, {"name": "Chaostreff Itzehoe", "city": "Itzehoe", "web": "https://www.cciz.de/", "display_name": "Computerclub Itzehoe", "location": [9.50196591631791, 53.9379972]}, {"name": "Chaostreff Jena", "city": "Jena", "web": "https://kraut.space/", "display_name": "Chaostreff Jena", "location": [11.5826767, 50.929203]}, {"name": "Chaostreff Klaus (Vorarlberg)", "city": "Klaus", "web": "https://open-lab.at/index.php/chaostreff", "display_name": "Chaostreff Klaus (Vorarlberg)", "location": [9.6460406, 47.304616]}, {"name": "Chaostreff Ludwigsburg", "city": "Ludwigsburg", "web": "https://complb.de/", "display_name": "Chaostreff Ludwigsburg", "location": [9.184755297842258, 48.8958537]}, {"name": "Chaostreff Marburg", "city": "Marburg", "web": "https://hsmr.cc/", "display_name": "Hackspace Marburg", "location": [8.7783888, 50.8161331]}, {"name": "Chaostreff M\u00fcnster", "city": "M\u00fcnster", "web": "http://www.warpzone.ms/", "display_name": "Chaostreff M\u00fcnster", "location": [7.6386827, 51.9446182]}, {"name": "Chaostreff N\u00fcrnberg", "city": "N\u00fcrnberg", "web": "https://chaostreff-nuernberg.de/", "display_name": "Chaostreff N\u00fcrnberg", "location": [11.081815196597816, 49.44836835]}, {"name": "Chaostreff Osnabr\u00fcck", "city": "Osnabr\u00fcck", "web": "https://chaostreff-osnabrueck.de/", "display_name": "Chaostreff Osnabr\u00fcck", "location": [8.047635, 52.2719595]}, {"name": "Chaostreff Potsdam", "city": "Potsdam", "web": "http://www.ccc-p.org/", "display_name": "Chaostreff Potsdam", "location": [13.078557, 52.3894652]}, {"name": "Chaostreff Recklinghausen", "city": "Recklinghausen", "web": "https://www.c3re.de/", "display_name": "Chaostreff Recklinghausen", "location": [7.1691246, 51.62435]}, {"name": "Chaostreff Rothenburg ob der Tauber", "city": "Rothenburg ob der Tauber", "web": "https://fablab-rothenburg.de/", "display_name": "FabLab Region Rothenburg ob der Tauber", "location": [10.1779991, 49.3783145]}, {"name": "Chaostreff Rotterdam", "city": "Rotterdam", "web": "https://www.pixelbar.nl/", "display_name": "Chaostreff Rotterdam", "location": [4.433639012034096, 51.9099513]}, {"name": "Chaostreff Schwerin", "city": "Schwerin", "web": "https://hacklabor.de/", "display_name": "Chaostreff Schwerin", "location": [11.4182505, 53.6010948]}, {"name": "Chaostreff Villingen-Schwenningen", "city": "Villingen-Schwenningen", "web": "https://vspace.one/", "display_name": "Chaostreff Villingen-Schwenningen", "location": [8.455688210119636, 48.06480785]}, {"name": "Chaostreff Winterthur", "city": "Winterthur", "web": "http://chaostreff.dingens.org/", "display_name": "Chaostreff Winterthur", "location": [8.7291498, 47.4991723]}, {"name": "Chaostreff Wuppertal", "city": "Wuppertal", "web": "https://chaostal.de/", "display_name": "Chaostal (Chaostreff Wuppertal)", "location": [7.144908700674055, 51.26671315]}, {"name": "Chaotikum", "city": "L\u00fcbeck", "web": "https://chaotikum.org/", "display_name": "Chaotikum", "location": [10.6713232, 53.8687751]}, {"name": "Datenburg", "city": "Bonn", "web": "https://datenburg.org/", "display_name": "Chaostreff Bonn", "location": [7.0987248, 50.7387823]}, {"name": "Hacksaar", "city": "Saarbr\u00fccken", "web": "https://hacksaar.de/", "display_name": "Chaostreff Saarbr\u00fccken", "location": [7.0357391, 49.279199]}, {"name": "Hackwerk Aalen", "city": "Aalen", "web": "https://aalen.space/", "display_name": "Chaostreff Aalen", "location": [10.0931765, 48.8362705]}, {"name": "Maschinendeck", "city": "Trier", "web": "https://maschinendeck.org/", "display_name": "Maschinendeck", "location": [6.6338265, 49.7529551]}, {"name": "Schaffenburg", "city": "Aschaffenburg", "web": "https://schaffenburg.org/", "display_name": "Schaffenburg", "location": [9.1358843, 49.9878536]}, {"name": "Section77", "city": "Offenburg", "web": "https://section77.de/", "display_name": "Section77", "location": [7.9458244, 48.4762239]}, {"name": "Technik.cafe", "city": "L\u00f6rrach", "web": "https://technik.cafe/", "display_name": "technik.cafe", "location": [7.6650755, 47.6155335]}, {"name": "UN-Hack-Bar", "city": "Unna", "web": "https://www.un-hack-bar.de/", "display_name": "UN-Hack-Bar", "location": [7.6917073, 51.5357811]}, {"name": "Westwoodlabs", "city": "Ransbach-Baumbach", "web": "https://westwoodlabs.de/", "display_name": "Chaostreff Westerwald", "location": [7.7252417, 50.4629465]}, {"name": "Dezentrale", "city": "Leipzig", "web": "https://dezentrale.space", "display_name": "dezentrale e.V.", "location": [12.3383176, 51.3365064]}, {"name": "FNordeingang", "city": "Neuss", "web": "https://fnordeingang.de/", "display_name": "fNordeingang", "location": [6.692534622093034, 51.186381499999996]}, {"name": "Gehacktes", "city": "Hoher Fl\u00e4ming", "web": "https://ge.hackt.es/", "display_name": "gehacktes - Chaostreff Hoher Fl\u00e4ming", "location": [12.40713030377876, 52.11842805]}, {"name": "HacKNology", "city": "Konstanz", "web": "https://www.hacknology.de/", "display_name": "hacKNology.de", "location": [9.1659379, 47.6594199]}, {"name": "Haxko", "city": "Andernach", "web": "https://haxko.space", "display_name": "Hacker- & Makerspace Koblenz", "location": [7.4044718, 50.4345198]}, {"name": "Z-Labor", "city": "Zwickau", "web": "http://z-labor.space/", "display_name": "Chaostreff z-Labor", "location": [12.480294009950217, 50.722194]}]
\ No newline at end of file
diff --git a/cache.example/erfa-info.json b/cache.example/erfa-info.json
index b0c44a0..cd20dc5 100644
--- a/cache.example/erfa-info.json
+++ b/cache.example/erfa-info.json
@@ -1 +1 @@
-{"Erlangen": {"location": [11.0028028, 49.6000372], "web": "https://erlangen.ccc.de/", "name": "Bits'n'Bugs"}, "Paderborn": {"location": [8.7479251, 51.7171873], "web": "https://c3pb.de/", "name": "Chaos Computer Club Paderborn"}, "Hamburg": {"location": [9.9443486, 53.5583644], "web": "https://hamburg.ccc.de/", "name": "Chaos Computer Club Hamburg"}, "Aachen": {"location": [6.091774700480718, 50.7715109], "web": "https://aachen.ccc.de/", "name": "Chaos Computer Club Aachen"}, "Basel": {"location": [7.6342977, 47.5323068], "web": "https://www.ccc-basel.ch/", "name": "Chaos Computer Club Basel"}, "Berlin": {"location": [13.38283, 52.5217046], "web": "https://berlin.ccc.de/", "name": "Chaos Computer Club Berlin"}, "Bremen": {"location": [8.7879452, 53.0864586], "web": "https://ccchb.de/", "name": "Chaos Computer Club Bremen"}, "K\u00f6ln": {"location": [6.912896, 50.9505492], "web": "https://koeln.ccc.de/", "name": "Chaos Computer Club Cologne"}, "Darmstadt": {"location": [8.6511275, 49.8708409], "web": "https://www.chaos-darmstadt.de/", "name": "Chaos Computer Club Darmstadt"}, "Dresden": {"location": [13.7288095, 51.0811269], "web": "https://c3d2.de/", "name": "Chaos Computer Club Dresden"}, "D\u00fcsseldorf": {"location": [6.7996122, 51.2125738], "web": "https://chaosdorf.de/", "name": "Chaos Computer Club D\u00fcsseldorf"}, "Frankfurt am Main": {"location": [8.6362035, 50.1241797], "web": "https://ccc-ffm.de/", "name": "Chaos Computer Club Frankfurt"}, "Freiburg": {"location": [7.8405746483681735, 47.99297755], "web": "https://cccfr.de/", "name": "Chaos Computer Club Freiburg"}, "G\u00f6ttingen": {"location": [9.9446185, 51.5453725], "web": "https://cccgoe.de/", "name": "Chaos Computer Club G\u00f6ttingen"}, "Hannover": {"location": [9.717936908887404, 52.38812135], "web": "https://hannover.ccc.de/", "name": "Chaos Computer Club Hannover"}, "Karlsruhe": {"location": [8.4073787, 49.0066019], "web": "https://entropia.de/", "name": "Chaos Computer Club Karlsruhe"}, "Mannheim": {"location": [8.4886818, 49.4636517], "web": "https://www.ccc-mannheim.de/", "name": "Chaos Computer Club Mannheim"}, "M\u00fcnchen": {"location": [11.5607949, 48.153629], "web": "https://www.muc.ccc.de/", "name": "Chaos Computer Club M\u00fcnchen"}, "Salzburg": {"location": [13.0561199, 47.7939873], "web": "https://cccsbg.at", "name": "Chaos Computer Club Salzburg"}, "Stuttgart": {"location": [9.1800132, 48.7784485], "web": "https://cccs.de/", "name": "Chaos Computer Club Stuttgart"}, "Ulm": {"location": [9.9910884, 48.4005863], "web": "https://ulm.ccc.de/", "name": "Chaos Computer Club Ulm"}, "Wien": {"location": [16.35611792351422, 48.209451099999995], "web": "https://c3w.at/", "name": "Chaos Computer Club Wien"}, "Wiesbaden": {"location": [8.2295012, 50.0831784], "web": "https://cccwi.de/", "name": "Chaos Computer Club Wiesbaden"}, "Z\u00fcrich": {"location": [8.5203159, 47.3869751], "web": "https://www.ccczh.ch/", "name": "Chaos Computer Club Z\u00fcrich"}, "Siegen": {"location": [8.0044503, 50.8689203], "web": "https://chaos-siegen.de/", "name": "Chaos Computer Club Siegen"}, "Kaiserslautern": {"location": [7.762277299724986, 49.44049735], "web": "http://www.chaos-inkl.de", "name": "Chaos inKL."}, "Essen": {"location": [7.024639594585695, 51.43860565], "web": "https://chaospott.de/"}, "Dortmund": {"location": [7.464966783180763, 51.52768425], "web": "https://www.chaostreff-dortmund.de/", "name": "Chaostreff Dortmund"}, "L\u00fcbeck": {"location": [10.6713232, 53.8687751], "web": "https://chaotikum.org/", "name": "Chaotikum"}, "Fulda": {"location": [9.6775152, 50.5588931], "web": "https://maglab.space/", "name": "Magrathea Laboratoratories"}, "W\u00fcrzburg": {"location": [9.923678752181619, 49.80223915], "web": "https://nerd2nerd.org/", "name": "Nerd2Nerd"}, "Bamberg": {"location": [10.89268892402827, 49.90189135], "web": "https://www.hackerspace-bamberg.de/", "name": "backspace"}, "Kassel": {"location": [9.484939, 51.3183203], "web": "https://flipdot.org/", "name": "flipdot"}}
\ No newline at end of file
+[{"name": "Bits'n'Bugs", "city": "Erlangen", "web": "https://erlangen.ccc.de/", "display_name": "Bits'n'Bugs", "location": [11.0028028, 49.6000372]}, {"name": "C3PB", "city": "Paderborn", "web": "https://c3pb.de/", "display_name": "Chaos Computer Club Paderborn", "location": [8.7479251, 51.7171873]}, {"name": "CCC Hansestadt Hamburg", "city": "Hamburg", "web": "https://hamburg.ccc.de/", "display_name": "Chaos Computer Club Hamburg", "location": [9.9443486, 53.5583644]}, {"name": "Chaos Computer Club Aachen", "city": "Aachen", "web": "https://aachen.ccc.de/", "display_name": "Chaos Computer Club Aachen", "location": [6.091774700480718, 50.7715109]}, {"name": "Chaos Computer Club Basel", "city": "Basel", "web": "https://www.ccc-basel.ch/", "display_name": "Chaos Computer Club Basel", "location": [7.6342977, 47.5323068]}, {"name": "Chaos Computer Club Berlin", "city": "Berlin", "web": "https://berlin.ccc.de/", "display_name": "Chaos Computer Club Berlin", "location": [13.38283, 52.5217046]}, {"name": "Chaos Computer Club Bremen", "city": "Bremen", "web": "https://ccchb.de/", "display_name": "Chaos Computer Club Bremen", "location": [8.7879452, 53.0864586]}, {"name": "Chaos Computer Club Cologne", "city": "K\u00f6ln", "web": "https://koeln.ccc.de/", "display_name": "Chaos Computer Club Cologne", "location": [6.912896, 50.9505492]}, {"name": "Chaos Computer Club Darmstadt", "city": "Darmstadt", "web": "https://www.chaos-darmstadt.de/", "display_name": "Chaos Computer Club Darmstadt", "location": [8.6511275, 49.8708409]}, {"name": "Chaos Computer Club Dresden", "city": "Dresden", "web": "https://c3d2.de/", "display_name": "Chaos Computer Club Dresden", "location": [13.7288095, 51.0811269]}, {"name": "Chaos Computer Club D\u00fcsseldorf", "city": "D\u00fcsseldorf", "web": "https://chaosdorf.de/", "display_name": "Chaos Computer Club D\u00fcsseldorf", "location": [6.7996122, 51.2125738]}, {"name": "Chaos Computer Club Frankfurt", "city": "Frankfurt am Main", "web": "https://ccc-ffm.de/", "display_name": "Chaos Computer Club Frankfurt", "location": [8.6362035, 50.1241797]}, {"name": "Chaos Computer Club Freiburg", "city": "Freiburg", "web": "https://cccfr.de/", "display_name": "Chaos Computer Club Freiburg", "location": [7.8405746483681735, 47.99297755]}, {"name": "Chaos Computer Club G\u00f6ttingen", "city": "G\u00f6ttingen", "web": "https://cccgoe.de/", "display_name": "Chaos Computer Club G\u00f6ttingen", "location": [9.9446185, 51.5453725]}, {"name": "Chaos Computer Club Hannover", "city": "Hannover", "web": "https://hannover.ccc.de/", "display_name": "Chaos Computer Club Hannover", "location": [9.717936908887404, 52.38812135]}, {"name": "Chaos Computer Club Karlsruhe", "city": "Karlsruhe", "web": "https://entropia.de/", "display_name": "Chaos Computer Club Karlsruhe", "location": [8.4073787, 49.0066019]}, {"name": "Chaos Computer Club Mannheim", "city": "Mannheim", "web": "https://www.ccc-mannheim.de/", "display_name": "Chaos Computer Club Mannheim", "location": [8.4886818, 49.4636517]}, {"name": "Chaos Computer Club M\u00fcnchen", "city": "M\u00fcnchen", "web": "https://www.muc.ccc.de/", "display_name": "Chaos Computer Club M\u00fcnchen", "location": [11.5607949, 48.153629]}, {"name": "Chaos Computer Club Salzburg", "city": "Salzburg", "web": "https://cccsbg.at", "display_name": "Chaos Computer Club Salzburg", "location": [13.0561199, 47.7939873]}, {"name": "Chaos Computer Club Stuttgart", "city": "Stuttgart", "web": "https://cccs.de/", "display_name": "Chaos Computer Club Stuttgart", "location": [9.1800132, 48.7784485]}, {"name": "Chaos Computer Club Ulm", "city": "Ulm", "web": "https://ulm.ccc.de/", "display_name": "Chaos Computer Club Ulm", "location": [9.9910884, 48.4005863]}, {"name": "Chaos Computer Club Wien", "city": "Wien", "web": "https://c3w.at/", "display_name": "Chaos Computer Club Wien", "location": [16.35611792351422, 48.209451099999995]}, {"name": "Chaos Computer Club Wiesbaden", "city": "Wiesbaden", "web": "https://cccwi.de/", "display_name": "Chaos Computer Club Wiesbaden", "location": [8.2295012, 50.0831784]}, {"name": "Chaos Computer Club Z\u00fcrich", "city": "Z\u00fcrich", "web": "https://www.ccczh.ch/", "display_name": "Chaos Computer Club Z\u00fcrich", "location": [8.5203159, 47.3869751]}, {"name": "Chaos Siegen", "city": "Siegen", "web": "https://chaos-siegen.de/", "display_name": "Chaos Computer Club Siegen", "location": [8.0044503, 50.8689203]}, {"name": "Chaos inKL.", "city": "Kaiserslautern", "web": "http://www.chaos-inkl.de", "display_name": "Chaos inKL.", "location": [7.762277299724986, 49.44049735]}, {"name": "Chaospott", "city": "Essen", "web": "https://chaospott.de/", "display_name": "Chaospott", "location": [7.024639594585695, 51.43860565]}, {"name": "Chaostreff Dortmund", "city": "Dortmund", "web": "https://www.chaostreff-dortmund.de/", "display_name": "Chaostreff Dortmund", "location": [7.464966783180763, 51.52768425]}, {"name": "Chaotikum", "city": "L\u00fcbeck", "web": "https://chaotikum.org/", "display_name": "Chaotikum", "location": [10.6713232, 53.8687751]}, {"name": "Magrathea Laboratories e.V.", "city": "Fulda", "web": "https://maglab.space/", "display_name": "Magrathea Laboratoratories", "location": [9.6775152, 50.5588931]}, {"name": "Nerd2nerd", "city": "W\u00fcrzburg", "web": "https://nerd2nerd.org/", "display_name": "Nerd2Nerd", "location": [9.923678752181619, 49.80223915]}, {"name": "Backspace", "city": "Bamberg", "web": "https://www.hackerspace-bamberg.de/", "display_name": "backspace", "location": [10.89268892402827, 49.90189135]}, {"name": "Flipdot", "city": "Kassel", "web": "https://flipdot.org/", "display_name": "flipdot", "location": [9.484939, 51.3183203]}]
\ No newline at end of file
diff --git a/generate_map.py b/generate_map.py
index c640918..1d6018f 100755
--- a/generate_map.py
+++ b/generate_map.py
@@ -2,6 +2,7 @@
#
# https://git.kabelsalat.ch/s3lph/erfamap
+import abc
import os
import urllib.request
import urllib.parse
@@ -54,74 +55,40 @@ class OutputPaths:
return os.path.relpath(path, start=self.path)
-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'
+class CoordinateTransform:
-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'
+ def __init__(self, projection: str):
+ self._proj = pyproj.Transformer.from_crs('epsg:4326', projection)
+ self.setup()
+
+ 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)
+
+ def __call__(self, lon, lat):
+ xt, yt = self._proj.transform(lon, lat)
+ return (
+ (xt - self._ox) * self._scalex,
+ (self._oy - yt) * self._scaley
+ )
-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
+class Drawable(abc.ABC):
+
+ def render(self, svg, texts):
+ pass
-def fetch_geoshapes(target, shape_urls):
- os.makedirs(target, exist_ok=True)
- 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():
- with open(os.path.join(target, item + '.json'), 'w') as f:
- json.dump(shape, f)
+class Country(Drawable):
-
-def fetch_wikidata_states(target):
- shape_urls = sparql_query('''
-
-PREFIX wd:
-PREFIX wdt:
-
-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
-}
-''')
- print('Retrieving state border shapes')
- fetch_geoshapes(target, shape_urls)
-
-
-def fetch_wikidata_countries(target):
- shape_urls = sparql_query('''
+ SVG_CLASS = 'country'
+ SPARQL_QUERY = '''
PREFIX wd:
PREFIX wdt:
@@ -142,134 +109,313 @@ SELECT DISTINCT ?item ?map WHERE {
FILTER (?stateclass = wd:Q6256 || ?stateclass = wd:Q3624078).
FILTER (?euroclass = wd:Q46 || ?euroclass = wd:Q8932).
}
-''')
- print('Retrieving country border shapes')
- fetch_geoshapes(target, shape_urls)
+'''
+ def __init__(self, ns, name, polygons):
+ self.name = name
+ self.polygons = [[ns.projection(lon, lat) for lon, lat in p] for p in polygons]
-def filter_boundingbox(source, target, bbox):
- files = os.listdir(source)
- os.makedirs(target, exist_ok=True)
- print('Filtering countries outside the bounding box')
- for f in tqdm.tqdm(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] >= bbox[0][0] and point[1] >= bbox[0][1] \
- and point[0] <= bbox[1][0] and point[1] <= bbox[1][1]:
- keep = True
+ def __len__(self):
+ return sum([len(p) for p in self.polygons])
+
+ def render(self, svg, texts):
+ for polygon in self.polygons:
+ points = ' '.join([f'{x},{y}' for x, y in polygon])
+ poly = etree.Element('polygon', points=points)
+ poly.set('class', self.__class__.SVG_CLASS)
+ poly.set('data-country', self.name)
+ svg.append(poly)
+
+ @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():
+ with open(os.path.join(target, item + '.json'), 'w') as f:
+ json.dump(shape, f)
+
+ @classmethod
+ def from_cache(cls, ns, source):
+ countries = []
+ files = os.listdir(source)
+ for f in files:
+ if not f.endswith('.json'):
+ continue
+ path = os.path.join(source, f)
+ with open(path, 'r') as sf:
+ shapedata = sf.read()
+ shape = json.loads(shapedata)
+ name = shape['description']['en']
+ geo = shape['data']['features'][0]['geometry']
+ if geo['type'] == 'Polygon':
+ geo['coordinates'] = [geo['coordinates']]
+ polygons = []
+ for poly in geo['coordinates']:
+ polygons.append(poly[0])
+ countries.append(cls(ns, name, polygons))
+ return countries
+
+ @classmethod
+ def filter_boundingbox(cls, ns, source, target, bbox):
+ files = os.listdir(source)
+ os.makedirs(target, exist_ok=True)
+ for f in tqdm.tqdm(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] >= bbox[0][0] and point[1] >= bbox[0][1] \
+ and point[0] <= bbox[1][0] and point[1] <= bbox[1][1]:
+ keep = True
+ break
+ if keep:
break
if keep:
- break
- if keep:
- with open(os.path.join(target, f), 'w') as sf:
- sf.write(shapedata)
+ with open(os.path.join(target, f), 'w') as sf:
+ sf.write(shapedata)
-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]
- # 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}')
+class FederalState(Country):
- for fmt in formats:
- response = locator.geocode(fmt)
- if response is not None:
- return response.longitude, response.latitude
+ SVG_CLASS = 'state'
+ SPARQL_QUERY = '''
- print(f'No location found for {name}, tried the following address formats:')
- for fmt in formats:
- print(f' {fmt}')
- return None
+PREFIX wd:
+PREFIX wdt:
+
+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
+}
+'''
-def fetch_erfas(target, url):
- 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())
- print('Looking up addresses')
- for name, erfa in tqdm.tqdm(erfadata['results'].items()):
- location = address_lookup(name, erfa['printouts'])
- if location is None:
- print(f'WARNING: No location for {name}')
- city = erfa['printouts']['Chaostreff-City'][0]
- 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]
- 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]
+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
+ 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:
- erfas[city]['name'] = name
-
-
- with open(target, 'w') as f:
- json.dump(erfas, f)
+ 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):
+ locator = Nominatim(user_agent=USER_AGENT)
+ 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]
-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]))
+ # 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}')
- 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)
- for data in erfadata.values():
- if 'location' not in data:
- continue
- lon, lat = data['location']
- 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)
- ]
+ for fmt in formats:
+ response = locator.geocode(fmt)
+ 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
+ def from_api(cls, name, attr, radius, ns):
+ city = attr['Chaostreff-City'][0]
+ location = cls.address_lookup(attr)
+ if location is None:
+ 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]
+ else:
+ 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
+
+ # Save to cache
+ with open(target, 'w') as f:
+ json.dump([e.to_dict() for e in erfas], f)
+ return erfas
+
+ @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]
+
+
+class Chaostreff(Erfa):
+
+ SMW_CATEGORY = 'Chaostreffs'
+
+ SVG_CLASS = 'chaostreff'
+ SVG_DATA = 'data-chaostreff'
+ SVG_DOTSIZE_ATTR = 'dotsize_treff'
+ SVG_LABEL = False
class BoundingBox:
@@ -395,7 +541,7 @@ class BoundingBox:
# Basically the weights correspond to geometrical distances,
# except for an actual collision, which gets a huge extra weight.
for o in other:
- if o.meta['city'] == self.meta['city']:
+ if o.meta['erfa'] == self.meta['erfa']:
continue
if o in self:
if o.finished:
@@ -406,17 +552,19 @@ class BoundingBox:
self._optimal = False
else:
maxs.append(max(pdist*2 - swm.chebyshev_distance(o), 0))
- for city, location in erfas.items():
- if city == self.meta['city']:
+ for erfa in erfas:
+ if erfa == self.meta['erfa']:
continue
+ location = (erfa.x, erfa.y)
if location in swe:
w += 1000
self._optimal = False
else:
- maxs.append(max(pdist*2 - swe.chebyshev_distance(o), 0))
- for city, location in chaostreffs.items():
- if city == self.meta['city']:
+ maxs.append(max(pdist*2 - swe.chebyshev_distance(location), 0))
+ for treff in chaostreffs:
+ if treff == self.meta['erfa']:
continue
+ location = (treff.x, treff.y)
if location in swc:
w += 1000
self._optimal = False
@@ -439,7 +587,35 @@ class BoundingBox:
return f'(({int(self.left)}, {int(self.top)}, {int(self.right)}, {int(self.bottom)}), weight={self.weight})'
-def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
+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)
+ ]
+
+
+def optimize_text_layout(ns, erfas, chaostreffs, width, svg):
# Load the font and measure its various different heights
font = ImageFont.truetype(ns.font, ns.font_size)
@@ -450,15 +626,15 @@ def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
# Generate a discrete set of text placement candidates around each erfa dot
candidates = {}
- for city, location in erfas.items():
- text = city
- erfax, erfay = location
+ for erfa in erfas:
+ city = erfa.city
+ text = erfa.city
for rfrom, to in ns.rename:
if rfrom == city:
text = to
break
- meta = {'city': city, 'text': text, 'baseline': capheight}
+ meta = {'erfa': erfa, '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]
candidates[city] = []
@@ -473,20 +649,20 @@ def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
if i == 0:
bw -= 0.003
bwl, bwr = bw, bw
- if erfax > 0.8 * size[0]:
+ if erfa.x > 0.8 * width:
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))
+ 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))
# 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.002),
- BoundingBox(erfax + ns.dotsize_erfa - mw, erfay + dist, width=mw, height=mh, meta=meta, base_weight=bw + 0.003),
+ 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),
])
# If debugging is enabled, render one rectangle around each label's bounding box, and one rectangle around each label's median box
@@ -500,7 +676,7 @@ def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
svg.append(dr)
- unfinished = {c for c in erfas.keys()}
+ unfinished = {e.city for e in erfas}
finished = {}
# Greedily choose a candidate for each label
@@ -527,7 +703,7 @@ def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
# 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']
+ mincity = minbox.meta['erfa'].city
finished[mincity] = minbox
minbox.finished = True
unfinished.discard(mincity)
@@ -553,9 +729,7 @@ def optimize_text_layout(ns, erfas, chaostreffs, size, svg):
return finished
-def create_imagemap(ns, size, parent,
- erfas, erfa_urls, erfa_names, texts,
- chaostreffs, chaostreff_urls, chaostreff_names):
+def create_imagemap(ns, size, parent, erfas, chaostreffs, texts):
s = ns.png_scale
img = etree.Element('img',
src=ns.output_directory.rel(ns.output_directory.png_path),
@@ -563,34 +737,24 @@ def create_imagemap(ns, size, parent,
width=str(size[0]*s), height=str(size[1]*s))
imgmap = etree.Element('map', name='erfamap')
- for city, location in erfas.items():
- if city not in erfa_urls:
+ for erfa in erfas:
+ if erfa.web is None:
continue
- box = texts[city]
area = etree.Element('area',
shape='circle',
- coords=f'{location[0]*s},{location[1]*s},{ns.dotsize_erfa*s}',
- href=erfa_urls[city])
- area2 = etree.Element('area',
- shape='rect',
- coords=f'{box.left*s},{box.top*s},{box.right*s},{box.bottom*s}',
- href=erfa_urls[city])
- if city in erfa_names:
- area.set('title', erfa_names[city])
- area2.set('title', erfa_names[city])
+ coords=f'{erfa.x*s},{erfa.y*s},{erfa.radius*s}',
+ href=erfa.web,
+ title=erfa.display_name)
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',
- coords=f'{location[0]*s},{location[1]*s},{ns.dotsize_treff*s}',
- href=chaostreff_urls[city])
- if city in chaostreff_names:
- area.set('title', chaostreff_names[city])
- imgmap.append(area)
+ 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)
parent.append(img)
parent.append(imgmap)
@@ -600,110 +764,26 @@ 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 = [
- (scalex*blt[0], scaley*trt[1]),
- (scalex*trt[0], scaley*blt[1])
- ]
- origin = trans_bounding_box[0]
- svg_box = (trans_bounding_box[1][0] - origin[0], origin[1] - trans_bounding_box[1][1])
-
- # Load state border lines from cached JSON files
- shapes_states = []
- files = os.listdir(ns.cache_directory.shapes_states)
- for f in files:
- if not f.endswith('.json'):
- continue
- path = os.path.join(ns.cache_directory.shapes_states, f)
- with open(path, 'r') as sf:
- shapedata = sf.read()
- shape = json.loads(shapedata)
- name = shape['description']['en']
- 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((name, ts))
-
- # Load country border lines from cached JSON files
- shapes_countries = []
- files = os.listdir(ns.cache_directory.shapes_filtered)
- for f in files:
- if not f.endswith('.json'):
- continue
- path = os.path.join(ns.cache_directory.shapes_filtered, f)
- with open(path, 'r') as sf:
- shapedata = sf.read()
- shape = json.loads(shapedata)
- name = shape['description']['en']
- 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((name, ts))
-
- # Load Erfa infos from cached JSON files
- erfas = {}
- erfa_urls = {}
- erfa_names = {}
- with open(ns.cache_directory.erfa_info, 'r') as f:
- ctdata = json.load(f)
- for city, data in ctdata.items():
- location = data.get('location')
- if location is None:
- continue
- xt, yt = transformer.transform(*location)
- erfas[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
- 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
-
- # Load Chaostreff infos from cached JSON files
- chaostreffs = {}
- chaostreff_urls = {}
- chaostreff_names = {}
- with open(ns.cache_directory.chaostreff_info, 'r') as f:
- ctdata = json.load(f)
- for city, data in ctdata.items():
- location = data.get('location')
- if location is None:
- continue
- 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
- xt, yt = transformer.transform(*location)
- chaostreffs[city] = (xt*scalex - origin[0], origin[1] - yt*scaley)
- 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
-
+ svg_box = ns.projection.setup(ns.scale_x, ns.scale_y, bbox=bbox)
rectbox = [0, 0, svg_box[0], svg_box[1]]
- for name, 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])
+
+ # Load everything from cached JSON files
+ countries = Country.from_cache(ns, ns.cache_directory.shapes_filtered)
+ 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:
+ for poly in c.polygons:
+ for x, y in poly:
+ rectbox[0] = min(x, rectbox[0])
+ rectbox[1] = min(y, rectbox[1])
+ rectbox[2] = max(x, rectbox[2])
+ rectbox[3] = max(y, rectbox[3])
print('Copying stylesheet and font')
dst = os.path.join(ns.output_directory.path, ns.stylesheet)
@@ -731,65 +811,19 @@ def create_svg(ns, bbox):
bg.set('class', 'background')
svg.append(bg)
- # Render country borders
- for name, shape in shapes_countries:
- points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
- poly = etree.Element('polygon', points=points)
- poly.set('class', 'country')
- poly.set('data-country', name)
- svg.append(poly)
-
- # Render state borders
- # Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
- for name, shape in sorted(shapes_states, key=lambda x: -sum(len(s) for s in x[1])):
- points = ' '.join([f'{lon},{lat}' for lon, lat in shape])
- poly = etree.Element('polygon', points=points)
- poly.set('class', 'state')
- poly.set('data-state', name)
- svg.append(poly)
-
# 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)
+ texts = optimize_text_layout(ns, erfas, chaostreffs, width=svg_box[0], svg=svg)
+
+ # 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)
# Render Erfa dots and their labels
- for city, location in erfas.items():
- 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)
- circle = etree.Element('circle', cx=str(location[0]), cy=str(location[1]), r=str(ns.dotsize_erfa))
- circle.set('class', 'erfa')
- circle.set('data-erfa', city)
- 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)
-
- # Render Chaostreff dots
- 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')
- circle.set('data-chaostreff', city)
- if city in chaostreff_names:
- title = etree.Element('title')
- title.text = chaostreff_names[city]
- circle.append(title)
- if city in chaostreff_urls:
- a = etree.Element('a', href=chaostreff_urls[city], target='_blank')
- a.append(circle)
- svg.append(a)
- else:
- svg.append(circle)
+ for erfa in erfas + chaostreffs:
+ erfa.render(svg, texts)
# Generate SVG, PNG and HTML output files
@@ -812,9 +846,7 @@ def create_svg(ns, bbox):
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)
+ create_imagemap(ns, svg_box, obj, erfas, chaostreffs, texts)
body.append(obj)
html.append(body)
with open(ns.output_directory.html_path, 'wb') as f:
@@ -830,9 +862,7 @@ def create_svg(ns, bbox):
body = etree.Element('body')
html.append(body)
- create_imagemap(ns, svg_box, body,
- erfas, erfa_urls, erfa_names, texts,
- chaostreffs, chaostreff_urls, chaostreff_names)
+ create_imagemap(ns, svg_box, body, erfas, chaostreffs, texts)
with open(ns.output_directory.imagemap_path, 'wb') as f:
f.write(b'\n')
@@ -856,7 +886,7 @@ def main():
ap.add_argument('--dotsize-erfa', type=float, default=13, help='Radius of Erfa dots')
ap.add_argument('--dotsize-treff', type=float, default=8, help='Radius of Chaostreff dots')
ap.add_argument('--rename', type=str, action='append', nargs=2, metavar=('FROM', 'TO'), default=[], help='Rename a city with an overly long name (e.g. "Rothenburg ob der Tauber" to "Rothenburg")')
- ap.add_argument('--projection', type=str, default='epsg:4258', help='Map projection to convert the WGS84 coordinates to')
+ ap.add_argument('--projection', type=CoordinateTransform, 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')
ap.add_argument('--png-scale', type=float, default=1.0, help='Scale of the PNG image')
@@ -867,26 +897,31 @@ def main():
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)
+ print('Retrieving country border shapes')
+ Country.fetch(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)
+ print('Retrieving state border shapes')
+ FederalState.fetch(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)
+ print('Retrieving Erfas information')
+ Erfa.fetch(ns, target=ns.cache_directory.erfa_info, radius=ns.dotsize_erfa)
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)
+ print('Retrieving Chaostreffs information')
+ Chaostreff.fetch(target=ns.cache_directory.chaostreff_info, radius=ns.dotsize_treff)
bbox = compute_bbox(ns)
- filter_boundingbox(ns.cache_directory.shapes_countries, ns.cache_directory.shapes_filtered, bbox)
+ print('Filtering countries outside the bounding box')
+ Country.filter_boundingbox(ns, ns.cache_directory.shapes_countries, ns.cache_directory.shapes_filtered, bbox)
create_svg(ns, bbox)
diff --git a/map.readme.png b/map.readme.png
index af18c1c..945aec4 100644
Binary files a/map.readme.png and b/map.readme.png differ