37 | Nice and easy proxy to remove some clutter from the TUM online iCal
38 | export. E.g.:
39 |
40 |
41 |
42 | Shorten Lesson Names like 'Grundlagen Betriebssysteme und
43 | Systemsoftware' → 'GBS'
44 |
45 |
46 | Adds locations, which are understood by Google Maps / Google Now
47 |
48 |
Replaces 'Tutorübung' with 'TÜ'
49 |
Remove event duplicates due to multiple rooms
50 |
Allows you to hide courses in your calendar without de-registering in TUMOnline
51 |
52 |
53 |
HowTo
54 |
55 |
56 | Navigate to your calendar in TUMOnline and grab the URL via the
57 | 'Veröffentlichen' button
58 |
59 |
60 |
61 |
62 |
68 |
71 |
72 |
73 |
The link is now copied to your clipboard!
74 |
Profit!
75 |
76 | Go to Google Calendar (or similar) and import the resulting url
77 |
78 |
79 |
80 |
81 |
Adjust Courses
82 |
83 | You can hide courses from your calendar by un-ticking the checkmarks next to them. This allows you to clean up your calendar without deregistering from the course in TUMOnline. Make sure to click the "Generate & Copy" button again after you have made your changes.
84 |
85 |
86 |
87 |
88 |
Contribute / Suggest
89 | If you want to suggest something create an issue at
90 | GitHub
91 |
92 |
93 |
94 |
95 | Version v2.0 -
97 | Changelog
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/internal/static/main.js:
--------------------------------------------------------------------------------
1 | const hiddenCourses = new Set();
2 | let originalLink = null;
3 |
4 | function getAndCheckCalLink() {
5 | let input = document.getElementById("tumCalLink")
6 | input.removeAttribute("class")
7 | let value = input.value;
8 | if (originalLink !== null) {
9 | value = originalLink;
10 | }
11 | if (!value.match(/https:\/\/campus\.tum\.de\/tumonlinej\/.{2}\/termin\/ical\?(pStud|pPers)=[0-9,A-Z]*&pToken=[0-9,A-Z]*/i)
12 | && !value.match(/https:\/\/cal\.tum\.(app|sexy)\/\?(pStud|pPers)=[0-9,A-Z]*&pToken=[0-9,A-Z]*/i)) {
13 | input.setAttribute("class", "invalid")
14 | return undefined;
15 | }
16 |
17 | return value;
18 | }
19 |
20 | function setCopyButton(state /* copied | reset */) {
21 | const btn = document.getElementById("generateLinkBtn");
22 |
23 | const isCopiedState = state === "copied";
24 | btn.innerText = isCopiedState ? "copied!" : "Generate & Copy";
25 | btn.setAttribute("style", `background-color: ${isCopiedState ? "#4CAF50" : "#007cea"};`);
26 | }
27 |
28 | function generateLink() {
29 | const calLink = getAndCheckCalLink();
30 | if (!calLink)
31 | return;
32 |
33 | const adjustedLink = new URL(calLink.replace(/https:\/\/campus.tum.de\/tumonlinej\/.{2}\/termin\/ical/i, "https://cal.tum.app").replace("\t", ""));
34 |
35 | // add course hide option
36 | const queryParams = new URLSearchParams(adjustedLink.search);
37 | for (const courseName of hiddenCourses) {
38 | queryParams.append("hide", courseName);
39 | }
40 |
41 | adjustedLink.search = queryParams;
42 | copyToClipboard(adjustedLink.toString());
43 | setCopyButton("copied");
44 |
45 | originalLink = calLink;
46 | document.getElementById("tumCalLink").value = adjustedLink.toString();
47 | }
48 |
49 | function reloadCourses() {
50 | originalLink = null;
51 | const calLink = getAndCheckCalLink();
52 | if (!calLink)
53 | return;
54 |
55 | // includes pStud and pToken
56 | const queryParams = new URLSearchParams(new URL(calLink).search);
57 | const url = new URL("api/courses", window.location.origin);
58 | url.search = queryParams;
59 |
60 | fetch(url)
61 | .then(response => {
62 | if (response.ok) {
63 | return response.json();
64 | }
65 |
66 | throw new Error(`Failed to fetch courses: ${response.text()}`);
67 | })
68 | .then(courses => {
69 | // add checkboxes for each course in courseAdjustList
70 | const courseAdjustList = document.getElementById("courseAdjustList");
71 | courseAdjustList.innerHTML = "";
72 |
73 | for (const [key, course] of Object.entries(courses)) {
74 | const li = document.createElement("li");
75 | const input = document.createElement("input");
76 | input.type = "checkbox";
77 | input.id = course.summary;
78 | input.checked = !course.hide;
79 | input.onchange = () => {
80 | if (input.checked) {
81 | hiddenCourses.delete(key);
82 | } else {
83 | hiddenCourses.add(key);
84 | }
85 | setCopyButton("reset");
86 | };
87 | li.appendChild(input);
88 | li.appendChild(document.createTextNode(course.summary));
89 | courseAdjustList.appendChild(li);
90 | }
91 |
92 | // enable/disable course adjustment section depending on whether courses were found
93 | document.getElementById("courseAdjustDiv").hidden = Object.keys(courses).length === 0;
94 | })
95 | .catch(err => {
96 | console.log(err);
97 | document.getElementById("courseAdjustDiv").hidden = true;
98 | });
99 | }
100 |
101 | function copyToClipboard(text) {
102 | const dummy = document.createElement("textarea");
103 | document.body.appendChild(dummy);
104 | dummy.value = text;
105 | dummy.select();
106 | document.execCommand("copy");
107 | document.body.removeChild(dummy);
108 | }
109 |
--------------------------------------------------------------------------------
/internal/courses.json:
--------------------------------------------------------------------------------
1 | {
2 | "Technology and Innovation Management": "TIM",
3 | "Tutorübungen": "TÜ",
4 | "Überfachliche Grundlagen": "ÜG",
5 | "Grundlagen": "G",
6 | "Introduction": "I",
7 | "Datenbanken": "DB",
8 | "Einsatz und Realisierung von Datenbanksystemen": "ERDB",
9 | "Zentralübungen": "ZÜ",
10 | "Zentralübung": "ZÜ",
11 | "Vertiefungsübungen": "VÜ",
12 | "Übungen": "Ü",
13 | "Übung": "Ü",
14 | "Exercise": "EX",
15 | "Exercises": "EX",
16 | "Anlagen-Zentralübung": "ZÜ",
17 | "Kleingruppenübung": "KGÜ",
18 | "Vertiefungsübung": "VÜ",
19 | "Vorlesung": "VL",
20 | "Gruppenübung": "GÜ",
21 | "Tutorübung": "TÜ",
22 | "Software Engineering für betriebliche Anwendungen - Bachelorkurs": "SEBA",
23 | "Software Engineering für betriebliche Anwendungen - Masterkurs: Web Application Engineering": "SEBA",
24 | "Betriebswirtschaftslehre": "BWL",
25 | "Volkswirtschaftslehre": "VWL",
26 | "Wirtschaftsprivatrecht 1": "WPR1",
27 | "Wirtschaftsprivatrecht 2": "WPR2",
28 | "Wirtschaftsprivatrecht": "WPR",
29 | "Funktionale Programmierung und Verifikation": "FPV",
30 | "Buchführung und Rechnungswesen": "BF & RW",
31 | "Planen und Entscheiden in betrieblichen Informationssystemen - Wirtschaftsinformatik 4": "PLEBIS",
32 | "Planen und Entscheiden in betrieblichen Informationssystemen": "PLEBIS",
33 | "Statistics for Business Administration (with Introduction to R)": "Stats",
34 | "Kostenrechnung für Wirtschaftsinformatik und Nebenfach": "KR",
35 | "Kostenrechnung": "KR",
36 | "Mathematische Behandlung der Natur- und Wirtschaftswissenschaften (Mathematik 1)": "MBNW",
37 | "Einführung in die Wirtschaftsinformatik": "WINFO",
38 | "Projektorganisation und -management in der Softwaretechnik": "POM",
39 | "Empirical Research Methods": "ERM",
40 | "Informationsmanagement": "IM",
41 | "Business Process Technologies and Management": "BPTM",
42 | "Bachelor-Seminar: Digitale Hochschule: Aktuelle Trends und Herausforderungen": "Digitale Hochschule",
43 | "Betriebssysteme und Systemsoftware": "BS",
44 | "Einführung in die Informatik 2": "Einführung in die Informatik 2",
45 | "Einführung in die Informatik": "EIDI",
46 | "Praktikum: Grundlagen der Programmierung": "PGdP",
47 | "Einführung in die Rechnerarchitektur": "ERA",
48 | "Grundlagenpraktikum: Rechnerarchitektur": "GRA",
49 | "Einführung in die Softwaretechnik": "EIST",
50 | "Grundlagen: Algorithmen und Datenstrukturen": "GAD",
51 | "Effiziente Algorithmen und Datenstrukturen": "EAD",
52 | "Grundlagen: Rechnernetze und Verteilte Systeme": "GRNVS",
53 | "Rechnernetze und Verteilte Systeme": "RNVS",
54 | "Einführung in die Theoretische Informatik": "Theo",
55 | "Diskrete Strukturen": "DS",
56 | "Diskrete Wahrscheinlichkeitstheorie": "DWT",
57 | "Numerisches Programmieren": "NumProg",
58 | "Modellbildung und Simulation": "ModSim",
59 | "(Fokus Analysis)": "(Ana)",
60 | "Lineare Algebra für Informatik": "LinAlg",
61 | "Analysis für Informatik": "Analysis",
62 | " der Künstlichen Intelligenz": "KI",
63 | "Advanced Topics of Software Engineering": "ASE",
64 | "Praktikum - iPraktikum, iOS Praktikum": "iPraktikum",
65 | "B1.1+B1.2 (intensiv)": "B1",
66 | "Business Analytics and Machine Learning": "BA & ML",
67 | "Netzsicherheit": "NetSec",
68 | "Management Accounting": "MA",
69 | "Advanced Seminar Finance & Accounting": "Seminar F&A",
70 | "Advanced Topics in Finance & Accounting": "Topics F&A",
71 | "Maschinelles Lernen": "ML",
72 | " to Deep Learning": "2DL",
73 | "Security Engineering": "SecE",
74 | "Peer-to-Peer-Systeme und Sicherheit": "P2PSec",
75 | "Requirements Engineering": "ReqE",
76 | "Functional Data Structures": "FDS",
77 | "Hardware Security": "HWSec",
78 | "Algorithmic Game Theory": "AGT",
79 | "Fortgeschrittene Themen des Softwaretests": "AdvTest",
80 | "Technische Mechanik I": "TM 1",
81 | "Technische Mechanik II": "TM 2",
82 | "Technische Mechanik": "TM",
83 | "Technischen Elektrizitätslehre": "TE",
84 | "Regelungstechnik": "RT",
85 | "Fluidmechanik I": "FM 1",
86 | "Fluidmechanik": "FM",
87 | "Maschinenelemente": "ME",
88 | "Werkstoffkunde": "WK",
89 | "Mathematik 1 für": "M1",
90 | "Mathematik 2 für": "M2",
91 | "Mathematik 3 für": "M3",
92 | "Technische Thermodynamik": "TTD",
93 | "Automatisierungstechnik 1": "AT",
94 | "Einführung in die Werkstoffe und Fertigungstechnologien von Carbon Composites": "CC",
95 | "Wärmetransportphänomene": "WTP",
96 | "Maschinenzeichnen": "MZ",
97 | "Informatikanwendungen in der Medizin": "CAMP",
98 | "Computer Vision": "CV",
99 | "Natural Language Processing": "NLP",
100 | "Augmented Reality": "AR",
101 | "Erweiterte Realität": "AR",
102 | "- Regeln des technischen Zeichnens (CAMPP)": "",
103 | " der modernen Informationstechnik I ": "dmIT 1",
104 | " der modernen Informationstechnik": "dmIT",
105 | "Modellierung von Unsicherheiten und Daten im Maschinenwesen": "MUD",
106 | "Mathematische Tools": "MTT",
107 | "Spanende Fertigungsverfahren": "SFV",
108 | "Hausaufgabentutorium": "HA TÜ",
109 | "Kurs zum/zur Fachsanitäter*in": "Fachsani",
110 | "Entrepreneurship for Students of Information Systems": "EShip",
111 | " ": " ",
112 | "&": "&",
113 | "IT und Unternehmensberatung": "ITUB",
114 | "Data Analytics in Applications ": "DAiA",
115 | "Strategisches IT-Management": "Strat. IT Mgmt.",
116 | "Fundamentals of Artificial Intelligence": "Fund. of AI",
117 | "Skizzier- und Darstellungstechniken": "SKDT"
118 | }
119 |
--------------------------------------------------------------------------------
/internal/app_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "io"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | ics "github.com/arran4/golang-ical"
10 | )
11 |
12 | func getTestData(t *testing.T, name string) (string, *App) {
13 | f, err := os.Open("testdata/" + name)
14 | if err != nil {
15 | t.Fatal("can't open testdata")
16 | }
17 | all, err := io.ReadAll(f)
18 | if err != nil {
19 | t.Fatal("can't read testdata")
20 | }
21 | app, err := newApp()
22 | if err != nil {
23 | t.Fatal("can't create test subject", err)
24 | }
25 | return string(all), app
26 | }
27 |
28 | func TestReplacement(t *testing.T) {
29 | r1 := Replacement{"b", "b"}
30 | if r1.isLessThan(&r1) {
31 | t.Error("Replacement should not be less than itself")
32 | return
33 | }
34 | if r1.isLessThan(&Replacement{key: "longer key first"}) {
35 | t.Error("Replacement should sort longer prefix first")
36 | return
37 | }
38 | if !r1.isLessThan(&Replacement{key: ""}) {
39 | t.Error("Replacement should sort longer prefix first")
40 | return
41 | }
42 | if r1.isLessThan(&Replacement{key: "a"}) {
43 | t.Error("Replacement should sort alphabetically")
44 | return
45 | }
46 | if !r1.isLessThan(&Replacement{key: "c"}) {
47 | t.Error("Replacement should sort alphabetically")
48 | return
49 | }
50 | if r1.isLessThan(&Replacement{key: r1.key, value: "a"}) {
51 | t.Error("Replacement with equal key should sort alphabetically by value")
52 | return
53 | }
54 | if !r1.isLessThan(&Replacement{key: r1.key, value: "c"}) {
55 | t.Error("Replacement with equal key should sort alphabetically by value")
56 | return
57 | }
58 |
59 | }
60 |
61 | func TestDeduplication(t *testing.T) {
62 | testData, app := getTestData(t, "duplication.ics")
63 | calendar, err := app.getCleanedCalendar([]byte(testData), map[string]bool{})
64 | if err != nil {
65 | t.Error(err)
66 | return
67 | }
68 | if len(calendar.Components) != 1 {
69 | t.Errorf("Calendar should have only 1 entry after deduplication but has %d", len(calendar.Components))
70 | return
71 | }
72 |
73 | // Verify that the additional room from the deduplicated event is in the description
74 | desc := calendar.Components[0].(*ics.VEvent).GetProperty(ics.ComponentPropertyDescription).Value
75 | if !strings.Contains(desc, "Additional rooms:") {
76 | t.Error("Description should contain 'Additional rooms:' when events are deduplicated with different locations")
77 | return
78 | }
79 | if !strings.Contains(desc, "MI HS 1") {
80 | t.Error("Description should contain the additional room 'MI HS 1' from the deduplicated event")
81 | return
82 | }
83 | }
84 |
85 | func TestMultipleRooms(t *testing.T) {
86 | // Setup app with building replacements
87 | app, err := newApp()
88 | if err != nil {
89 | t.Fatal(err)
90 | }
91 |
92 | // Create a dummy event with multiple rooms
93 | event := ics.NewEvent("test-uid")
94 |
95 | // Using 5508 and 5612 which are present in buildings.json
96 | // 5508 -> Boltzmannstr. 15, 85748 Garching b. München
97 | // 5612 -> Boltzmannstr. 3, 85748 Garching b. München
98 |
99 | // Construct a location string with multiple rooms.
100 | // RoomA, DescA (5508.01.001), RoomB, DescB (5612.01.001)
101 |
102 | location := "RoomA, DescA (5508.01.001), RoomB, DescB (5612.01.001)"
103 | event.SetProperty(ics.ComponentPropertyLocation, location)
104 | event.SetProperty(ics.ComponentPropertySummary, "Test Event")
105 | event.SetProperty(ics.ComponentPropertyDescription, "Original Description")
106 | event.SetProperty(ics.ComponentPropertyStatus, "CONFIRMED")
107 |
108 | app.cleanEvent(event, []string{})
109 |
110 | desc := event.GetProperty(ics.ComponentPropertyDescription).Value
111 | loc := event.GetProperty(ics.ComponentPropertyLocation).Value
112 |
113 | // Check if both rooms are present in description or nav links
114 | if !strings.Contains(desc, "5508.01.001") {
115 | t.Errorf("Description should contain first room ID")
116 | }
117 | if !strings.Contains(desc, "5612.01.001") {
118 | t.Errorf("Description should contain second room ID")
119 | }
120 |
121 | // Check if nav links are generated for both
122 | // 5508.01.001 -> https://nav.tum.de/room/5508.01.001
123 | // 5612.01.001 -> https://nav.tum.de/room/5612.01.001
124 |
125 | if !strings.Contains(desc, "https://nav.tum.de/room/5508.01.001") {
126 | t.Error("Missing nav link for first room")
127 | }
128 | if !strings.Contains(desc, "https://nav.tum.de/room/5612.01.001") {
129 | t.Error("Missing nav link for second room")
130 | }
131 |
132 | // With non-greedy regex, the location should be the first building (5508)
133 | expectedLoc := "Boltzmannstr. 15, 85748 Garching b. München"
134 | if loc != expectedLoc {
135 | t.Errorf("Location should be %s but is %s", expectedLoc, loc)
136 | }
137 | }
138 |
139 | func TestNameShortening(t *testing.T) {
140 | testData, app := getTestData(t, "nameshortening.ics")
141 | calendar, err := app.getCleanedCalendar([]byte(testData), map[string]bool{})
142 | if err != nil {
143 | t.Error(err)
144 | return
145 | }
146 | summary := calendar.Components[0].(*ics.VEvent).GetProperty(ics.ComponentPropertySummary).Value
147 | if summary != "ERA" {
148 | t.Errorf("Einführung in die Rechnerarchitektur (IN0004) VO, Standardgruppe should be shortened to ERA but is %s", summary)
149 | return
150 | }
151 | }
152 |
153 | func TestLocationReplacement(t *testing.T) {
154 | testData, app := getTestData(t, "location.ics")
155 | calendar, err := app.getCleanedCalendar([]byte(testData), map[string]bool{})
156 | if err != nil {
157 | t.Error(err)
158 | return
159 | }
160 | location := calendar.Components[0].(*ics.VEvent).GetProperty(ics.ComponentPropertyLocation).Value
161 | expectedLocation := "Boltzmannstr. 15, 85748 Garching b. München"
162 | if location != expectedLocation {
163 | t.Errorf("Location should be shortened to %s but is %s", expectedLocation, location)
164 | return
165 | }
166 | desc := calendar.Components[0].(*ics.VEvent).GetProperty(ics.ComponentPropertyDescription).Value
167 | expectedDescription := "Additional rooms:\nMI HS 1\n\nhttps://nav.tum.de/room/5508.02.801\nMW 1801, Ernst-Schmidt-Hörsaal (5508.02.801)\nEinführung in die Rechnerarchitektur (IN0004) VO, Standardgruppe\nfix; Abhaltung;"
168 | if desc != expectedDescription {
169 | t.Errorf("Description should be \n\n%s\n\nbut is\n\n%s\n\n", expectedDescription, desc)
170 | return
171 | }
172 | }
173 |
174 | func TestCourseFiltering(t *testing.T) {
175 | testData, app := getTestData(t, "coursefiltering.ics")
176 |
177 | // make sure the unfiltered calendar has 2 entries
178 | fullCalendar, err := app.getCleanedCalendar([]byte(testData), map[string]bool{})
179 | if err != nil {
180 | t.Error(err)
181 | return
182 | }
183 | if len(fullCalendar.Components) != 2 {
184 | t.Errorf("Calendar should have 2 entries before course filtering but has %d", len(fullCalendar.Components))
185 | return
186 | }
187 |
188 | // now filter out one course
189 | filter := "Einführung in die Rechnerarchitektur (IN0004) VO, Standardgruppe"
190 | filteredCalendar, err := app.getCleanedCalendar([]byte(testData), map[string]bool{filter: true})
191 | if err != nil {
192 | t.Error(err)
193 | return
194 | }
195 | if len(filteredCalendar.Components) != 1 {
196 | t.Errorf("Calendar should have only 1 entry after course filtering but has %d", len(filteredCalendar.Components))
197 | return
198 | }
199 |
200 | // make sure the summary does not contain the filtered course's name
201 | summary := filteredCalendar.Components[0].(*ics.VEvent).GetProperty(ics.ComponentPropertySummary).Value
202 | if strings.Contains(summary, filter) {
203 | t.Errorf("Summary should not contain %s but is %s", filter, summary)
204 | return
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/arran4/golang-ical v0.3.2 h1:MGNjcXJFSuCXmYX/RpZhR2HDCYoFuK8vTPFLEdFC3JY=
2 | github.com/arran4/golang-ical v0.3.2/go.mod h1:xblDGxxIUMWwFZk9dlECUlc1iXNV65LJZOTHLVwu8bo=
3 | github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
4 | github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
5 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
6 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
7 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
8 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
13 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
14 | github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
15 | github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
16 | github.com/getsentry/sentry-go/gin v0.40.0 h1:kMezKwVF/qdnqp+f5FPM6vIbQeAW13/1Ay/ohP301i8=
17 | github.com/getsentry/sentry-go/gin v0.40.0/go.mod h1:CKVBUdzTvDDbywpGQgT2c1ZsAYOc7pULmsDME/Z3O84=
18 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
19 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
20 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
21 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
22 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
23 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
24 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
25 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
26 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
27 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
28 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
29 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
30 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
31 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
32 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
33 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
34 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
35 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
36 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
37 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
39 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
40 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
41 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
42 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
43 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
44 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
45 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
46 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
47 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
50 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
51 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
52 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
53 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
54 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
55 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
56 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
57 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
60 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
61 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
62 | github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
63 | github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
64 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
65 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
66 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
67 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
68 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
69 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
70 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
71 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
72 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
73 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
74 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
75 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
76 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
77 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
78 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
79 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
80 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
81 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
82 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
83 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
84 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
85 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
86 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
87 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
88 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
89 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
90 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
91 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
92 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
93 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
94 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
95 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
96 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
97 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
98 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
99 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
104 |
--------------------------------------------------------------------------------
/internal/app.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "embed"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "log"
10 | "net/http"
11 | "net/url"
12 | "regexp"
13 | "sort"
14 | "strings"
15 |
16 | ics "github.com/arran4/golang-ical"
17 | "github.com/getsentry/sentry-go"
18 | sentrygin "github.com/getsentry/sentry-go/gin"
19 | "github.com/gin-gonic/gin"
20 | )
21 |
22 | //go:embed courses.json
23 | var coursesJson string
24 |
25 | //go:embed buildings.json
26 | var buildingsJson string
27 |
28 | //go:embed static
29 | var static embed.FS
30 |
31 | // Version is injected at build time by the compiler with the correct git-commit-sha or "dev" in development
32 | var Version = "dev"
33 |
34 | type App struct {
35 | engine *gin.Engine
36 |
37 | courseReplacements []*Replacement
38 | buildingReplacements map[string]string
39 | }
40 |
41 | type Replacement struct {
42 | key string
43 | value string
44 | }
45 |
46 | type Course struct {
47 | Summary string `json:"summary"`
48 | Hide bool `json:"hide"`
49 | }
50 |
51 | // for sorting replacements by length, then alphabetically
52 | func (r1 *Replacement) isLessThan(r2 *Replacement) bool {
53 | if len(r1.key) != len(r2.key) {
54 | return len(r1.key) > len(r2.key)
55 | }
56 | if r1.key != r2.key {
57 | return r1.key < r2.key
58 | }
59 | return r1.value < r2.value
60 | }
61 |
62 | func newApp() (*App, error) {
63 | a := App{}
64 |
65 | // courseReplacements is a map of course names to shortened names.
66 | // We sort it by length, then alphabetically to ensure a consistent execution order
67 | var rawCourseReplacements map[string]string
68 | if err := json.Unmarshal([]byte(coursesJson), &rawCourseReplacements); err != nil {
69 | return nil, err
70 | }
71 | for key, value := range rawCourseReplacements {
72 | a.courseReplacements = append(a.courseReplacements, &Replacement{key, value})
73 | }
74 | sort.Slice(a.courseReplacements, func(i, j int) bool { return a.courseReplacements[i].isLessThan(a.courseReplacements[j]) })
75 | // buildingReplacements is a map of room numbers to building names
76 | if err := json.Unmarshal([]byte(buildingsJson), &a.buildingReplacements); err != nil {
77 | return nil, err
78 | }
79 | return &a, nil
80 | }
81 |
82 | func customLogFormatter(params gin.LogFormatterParams) string {
83 | return fmt.Sprintf("[GIN] %v |%s %3d %s | %13v | %15s |%s %-7s%s %#v\n%s",
84 | params.TimeStamp.Format("2006/01/02 - 15:04:05"),
85 | params.StatusCodeColor(),
86 | params.StatusCode,
87 | params.ResetColor(),
88 | params.Latency,
89 | params.ClientIP,
90 | params.MethodColor(),
91 | params.Method,
92 | params.ResetColor(),
93 | hideTokens(params.Path),
94 | params.ErrorMessage,
95 | )
96 | }
97 |
98 | func hideTokens(path string) string {
99 | u, err := url.Parse(path)
100 | if err != nil {
101 | return path
102 | }
103 |
104 | pStud := u.Query().Get("pStud")
105 | pPers := u.Query().Get("pPers")
106 | pToken := u.Query().Get("pToken")
107 |
108 | if pToken == "" || (pStud == "" && pPers == "") {
109 | return path
110 | }
111 |
112 | manyXes := strings.Repeat("X", 12)
113 | tokenReplaced := pToken[:4] + manyXes
114 | if pStud != "" {
115 | return fmt.Sprintf("/?pStud=%s&pToken=%s", pStud[:4]+manyXes, tokenReplaced)
116 | }
117 | return fmt.Sprintf("/?pPers=%s&pToken=%s", pPers[:4]+manyXes, tokenReplaced)
118 | }
119 |
120 | func (a *App) Run() error {
121 | if err := sentry.Init(sentry.ClientOptions{
122 | Dsn: "https://2fbc80ad1a99406cb72601d6a47240ce@glitch.exgen.io/4",
123 | Release: Version,
124 | AttachStacktrace: true,
125 | EnableTracing: true,
126 | // Specify a fixed sample rate: 10% will do for now
127 | TracesSampleRate: 0.1,
128 | }); err != nil {
129 | fmt.Printf("Sentry initialization failed: %v\n", err)
130 | }
131 |
132 | // Create app struct
133 | a, err := newApp()
134 | if err != nil {
135 | return err
136 | }
137 |
138 | // Setup Gin with sentry traces, logger and routes
139 | gin.SetMode("release")
140 | a.engine = gin.New()
141 | a.engine.Use(sentrygin.New(sentrygin.Options{}))
142 | logger := gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/health"}, Formatter: customLogFormatter})
143 | a.engine.Use(logger, gin.Recovery())
144 | a.configRoutes()
145 |
146 | // Start the engines
147 | return a.engine.Run(":4321")
148 | }
149 |
150 | func (a *App) configRoutes() {
151 | a.engine.GET("/api/courses", a.handleGetCourses)
152 | a.engine.GET("/health", func(c *gin.Context) {
153 | c.JSON(http.StatusOK, gin.H{
154 | "status": "ok",
155 | })
156 | })
157 | a.engine.Any("/", a.handleIcal)
158 | f := http.FS(static)
159 | a.engine.StaticFS("/files/", f)
160 | a.engine.NoMethod(func(c *gin.Context) {
161 | c.AbortWithStatus(http.StatusNotImplemented)
162 | })
163 | }
164 |
165 | func getUrl(c *gin.Context) string {
166 | stud := c.Query("pStud")
167 | pers := c.Query("pPers")
168 | token := c.Query("pToken")
169 | if (stud == "" && pers == "") || token == "" {
170 | // Missing parameters: just serve our landing page
171 | f, err := static.Open("static/index.html")
172 | if err != nil {
173 | sentry.CaptureException(err)
174 | c.AbortWithStatusJSON(http.StatusInternalServerError, err)
175 | return ""
176 | }
177 |
178 | if _, err := io.Copy(c.Writer, f); err != nil {
179 | sentry.CaptureException(err)
180 | c.AbortWithStatusJSON(http.StatusInternalServerError, err)
181 | return ""
182 | }
183 | return ""
184 | }
185 | if stud == "" {
186 | return fmt.Sprintf("https://campus.tum.de/tumonlinej/ws/termin/ical?pPers=%s&pToken=%s", pers, token)
187 | }
188 | return fmt.Sprintf("https://campus.tum.de/tumonlinej/ws/termin/ical?pStud=%s&pToken=%s", stud, token)
189 | }
190 |
191 | func getCalendar(ctx *gin.Context) ([]byte, map[string]bool, error) {
192 | fetchURL := getUrl(ctx)
193 | if fetchURL == "" {
194 | return nil, nil, errors.New("no fetchable URL passed")
195 | }
196 | resp, err := http.Get(fetchURL)
197 | if err != nil {
198 | return nil, nil, fmt.Errorf("can't fetch calendar: %w", err)
199 | }
200 | all, err := io.ReadAll(resp.Body)
201 | if err != nil {
202 | return nil, nil, fmt.Errorf("can't read calendar: %w", err)
203 | }
204 |
205 | // Create map of all hidden courses
206 | hide := ctx.QueryArray("hide")
207 | hiddenCourses := make(map[string]bool)
208 | for _, course := range hide {
209 | hiddenCourses[course] = true
210 | }
211 |
212 | return all, hiddenCourses, nil
213 | }
214 |
215 | // handleIcal returns a filtered calendar with all courses that are currently offered on campus.
216 | func (a *App) handleIcal(ctx *gin.Context) {
217 | allEvents, hiddenCourses, err := getCalendar(ctx)
218 | if err != nil {
219 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, err)
220 | return
221 | }
222 |
223 | cleaned, err := a.getCleanedCalendar(allEvents, hiddenCourses)
224 | if err != nil {
225 | ctx.AbortWithStatus(http.StatusInternalServerError)
226 | return
227 | }
228 |
229 | response := []byte(cleaned.Serialize())
230 | ctx.Header("Content-Type", "text/calendar")
231 | ctx.Header("Content-Length", fmt.Sprintf("%d", len(response)))
232 |
233 | if _, err := ctx.Writer.Write(response); err != nil {
234 | sentry.CaptureException(err)
235 | }
236 | }
237 |
238 | // handleGetCourses returns a list of all courses that are currently offered on campus.
239 | // This is used to populate the dropdown in the landing page for hiding courses.
240 | func (a *App) handleGetCourses(ctx *gin.Context) {
241 | allEvents, hidden, err := getCalendar(ctx)
242 | if err != nil {
243 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, err)
244 | return
245 | }
246 |
247 | cal, err := a.getCleanedCalendar(allEvents, map[string]bool{})
248 | if err != nil {
249 | ctx.AbortWithStatus(http.StatusInternalServerError)
250 | return
251 | }
252 |
253 | // detect all courses, de-duplicate them by their summary (lecture name)
254 | courses := make(map[string]Course)
255 | for _, component := range cal.Components {
256 | switch component.(type) {
257 | case *ics.VEvent:
258 | eventSummary := cleanEventSummary(component.(*ics.VEvent).GetProperty(ics.ComponentPropertySummary).Value)
259 | if _, exists := courses[eventSummary]; !exists {
260 | courses[eventSummary] = Course{
261 | Summary: eventSummary,
262 | // Check for existing hidden course, that might want to be updated
263 | Hide: hidden[eventSummary],
264 | }
265 | }
266 | log.Printf("summaries: %s", eventSummary)
267 | default:
268 | continue
269 | }
270 | }
271 |
272 | ctx.JSON(http.StatusOK, courses)
273 | }
274 |
275 | func (a *App) getCleanedCalendar(all []byte, hiddenCourses map[string]bool) (*ics.Calendar, error) {
276 | cal, err := ics.ParseCalendar(strings.NewReader(string(all)))
277 | if err != nil {
278 | return nil, err
279 | }
280 |
281 | // First pass: collect all locations for each dedup key (lecture name + datetime)
282 | // This allows us to show additional rooms in the description when events are deduplicated
283 | eventLocations := make(map[string][]string)
284 | for _, component := range cal.Components {
285 | switch component.(type) {
286 | case *ics.VEvent:
287 | event := component.(*ics.VEvent)
288 | eventSummary := event.GetProperty(ics.ComponentPropertySummary).Value
289 | if hiddenCourses[eventSummary] {
290 | continue
291 | }
292 | dedupKey := fmt.Sprintf("%s-%s", eventSummary, event.GetProperty(ics.ComponentPropertyDtStart))
293 | if l := event.GetProperty(ics.ComponentPropertyLocation); l != nil && l.Value != "" {
294 | eventLocations[dedupKey] = append(eventLocations[dedupKey], l.Value)
295 | }
296 | }
297 | }
298 |
299 | // Second pass: deduplicate and clean events, adding additional rooms to the description
300 | hasLecture := make(map[string]bool)
301 | var newComponents []ics.Component // saves the components we keep because they are not duplicated
302 |
303 | for _, component := range cal.Components {
304 | switch component.(type) {
305 | case *ics.VEvent:
306 | event := component.(*ics.VEvent)
307 |
308 | // check if the summary contains any of the hidden keys, and if yes, skip it
309 | eventSummary := event.GetProperty(ics.ComponentPropertySummary).Value
310 | if hiddenCourses[eventSummary] {
311 | continue
312 | }
313 |
314 | // deduplicate lectures by their summary and datetime
315 | dedupKey := fmt.Sprintf("%s-%s", event.GetProperty(ics.ComponentPropertySummary).Value, event.GetProperty(ics.ComponentPropertyDtStart))
316 | if _, ok := hasLecture[dedupKey]; ok {
317 | continue
318 | }
319 | hasLecture[dedupKey] = true // mark event as seen
320 |
321 | // Get additional locations from duplicated events (skip the current event's location and duplicates)
322 | var additionalLocations []string
323 | if locations := eventLocations[dedupKey]; len(locations) > 1 {
324 | currentLocation := ""
325 | if l := event.GetProperty(ics.ComponentPropertyLocation); l != nil {
326 | currentLocation = l.Value
327 | }
328 | seen := make(map[string]bool)
329 | seen[currentLocation] = true
330 | for _, loc := range locations {
331 | if !seen[loc] {
332 | seen[loc] = true
333 | additionalLocations = append(additionalLocations, loc)
334 | }
335 | }
336 | }
337 |
338 | // clean up the event (with additional locations for the description)
339 | a.cleanEvent(event, additionalLocations)
340 | newComponents = append(newComponents, event)
341 | default: // keep everything that is not an event (metadata etc.)
342 | newComponents = append(newComponents, component)
343 | }
344 | }
345 | cal.Components = newComponents
346 | return cal, nil
347 | }
348 |
349 | // matches tags like (IN0001) or [MA2012] and everything after.
350 | // unfortunate also matches wrong brackets like [MA123) but hey…
351 | var reTag = regexp.MustCompile(" ?[\\[(](ED|MW|SOM|CIT|MA|IN|WI|WIB)[0-9]+((_|-|,)[a-zA-Z0-9]+)*[\\])].*")
352 |
353 | // Matches location and teacher from language course title
354 | var reLoc = regexp.MustCompile(" ?(München|Garching|Weihenstephan).+")
355 |
356 | // Matches repeated whitespaces
357 | var reSpace = regexp.MustCompile(`\s\s+`)
358 |
359 | // Matches unique starting numbers like "0000002467 " in "0000002467 Semantik"
360 | var reWeirdStartingNumbers = regexp.MustCompile(`^0\d+ `)
361 |
362 | var unneeded = []string{
363 | "Standardgruppe",
364 | "PR",
365 | "VO",
366 | "FA",
367 | "VI",
368 | "TT",
369 | "UE",
370 | "SE",
371 | "(Limited places)",
372 | "(Online)",
373 | }
374 |
375 | var reRoom = regexp.MustCompile("^(.*?),.*?(\\d{4})\\.(?:\\d\\d|EG|UG|DG|Z\\d|U\\d)\\.\\d+")
376 |
377 | // matches strings like: (5612.03.017), (5612.EG.017), (5612.EG.010B)
378 | var reNavigaTUM = regexp.MustCompile("\\(\\d{4}\\.[a-zA-Z0-9]{2}\\.\\d{3}[A-Z]?\\)")
379 |
380 | func (a *App) cleanEvent(event *ics.VEvent, additionalLocations []string) {
381 | // Event Title
382 | summary := ""
383 | if s := event.GetProperty(ics.ComponentPropertySummary); s != nil {
384 | summary = cleanEventSummary(s.Value)
385 | }
386 | originalSummary := summary
387 |
388 | // Remove the TAG and anything after e.g.: (IN0001) or [MA0001]
389 | summary = reTag.ReplaceAllString(summary, "")
390 | // remove location and teacher from the language course title
391 | summary = reLoc.ReplaceAllString(summary, "")
392 | summary = reSpace.ReplaceAllString(summary, "")
393 | for _, replace := range unneeded {
394 | summary = strings.ReplaceAll(summary, replace, "")
395 | }
396 | // sometimes the summary has weird numbers attached like "0000002467 " in "0000002467 Semantik"
397 | // What the heck? And why only sometimes???
398 | summary = reWeirdStartingNumbers.ReplaceAllString(summary, "")
399 |
400 | // Do all the course-specific replacements
401 | for _, repl := range a.courseReplacements {
402 | summary = strings.ReplaceAll(summary, repl.key, repl.value)
403 | }
404 | event.SetSummary(summary)
405 |
406 | // Description
407 | // Remember the old title in the description
408 | description := ""
409 | if d := event.GetProperty(ics.ComponentPropertyDescription); d != nil {
410 | description = d.Value
411 | }
412 | description = originalSummary + "\n" + description
413 |
414 | // Location
415 | // Replace the location with the building name, if it matches our map
416 | location := ""
417 | if l := event.GetProperty(ics.ComponentPropertyLocation); l != nil {
418 | location = event.GetProperty(ics.ComponentPropertyLocation).Value
419 | }
420 | results := reRoom.FindStringSubmatch(location)
421 | if len(results) == 3 {
422 | if building, ok := a.buildingReplacements[results[2]]; ok {
423 | description = location + "\n" + description
424 | event.SetLocation(building)
425 | }
426 | if roomIDs := reNavigaTUM.FindAllString(location, -1); len(roomIDs) > 0 {
427 | for _, roomID := range roomIDs {
428 | roomID = strings.Trim(roomID, "()")
429 | description = fmt.Sprintf("https://nav.tum.de/room/%s\n%s", roomID, description)
430 | }
431 | }
432 | }
433 |
434 | // Add additional locations from deduplicated events to the description
435 | if len(additionalLocations) > 0 {
436 | description = "Additional rooms:\n" + strings.Join(additionalLocations, "\n") + "\n\n" + description
437 | }
438 | event.SetDescription(description)
439 |
440 | // Set status based on ical status, so cancelled events are marked as such in the calendar
441 | switch event.GetProperty(ics.ComponentPropertyStatus).Value {
442 | case "CONFIRMED":
443 | event.SetStatus(ics.ObjectStatusConfirmed)
444 | case "CANCELLED":
445 | event.SetStatus(ics.ObjectStatusCancelled)
446 | case "TENTATIVE":
447 | event.SetStatus(ics.ObjectStatusTentative)
448 | }
449 | }
450 |
451 | func cleanEventSummary(eventSummary string) string {
452 | eventSummary = strings.TrimSpace(eventSummary)
453 | eventSummary = strings.TrimSuffix(eventSummary, " ,")
454 | return eventSummary
455 | }
456 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/internal/buildings.json:
--------------------------------------------------------------------------------
1 | {
2 | "0101": "Theresienstr. 90, 80333 M\u00fcnchen",
3 | "0102": "Theresienstr. 90, 80333 M\u00fcnchen",
4 | "0103": "Theresienstr. 90, 80333 M\u00fcnchen",
5 | "0104": "Theresienstr. 90, 80333 M\u00fcnchen",
6 | "0105": "Theresienstr. 90, 80333 M\u00fcnchen",
7 | "0106": "Theresienstr. 90, 80333 M\u00fcnchen",
8 | "0108": "Theresienstr. 90, 80333 M\u00fcnchen",
9 | "0109": "Theresienstr. 90, 80333 M\u00fcnchen",
10 | "0201": "Gabelsbergerstr. 43, 80333 M\u00fcnchen",
11 | "0202": "Gabelsbergerstr. 39, 80333 M\u00fcnchen",
12 | "0203": "Gabelsbergerstr. 45, 80333 M\u00fcnchen",
13 | "0204": "Gabelsbergerstr. 49, 80333 M\u00fcnchen",
14 | "0205": "Arcisstr. 19, 80333 M\u00fcnchen",
15 | "0206": "Arcisstr. 17, 80333 M\u00fcnchen",
16 | "0305": "Barerstr. 21, 80333 M\u00fcnchen",
17 | "0401": "Richard-Wagner-Str. 18, 80333 M\u00fcnchen",
18 | "0403": "Richard-Wagner-Str. 14, 80333 M\u00fcnchen",
19 | "0501": "Arcisstr. 21, 80333 M\u00fcnchen",
20 | "0502": "Arcisstr. 21, 80333 M\u00fcnchen",
21 | "0503": "Arcisstr. 21, 80333 M\u00fcnchen",
22 | "0504": "Arcisstr. 21, 80333 M\u00fcnchen",
23 | "0505": "Arcisstr. 21, 80333 M\u00fcnchen",
24 | "0506": "Arcisstr. 21, 80333 M\u00fcnchen",
25 | "0507": "Arcisstr. 21, 80333 M\u00fcnchen",
26 | "0508": "Arcisstr. 21, 80333 M\u00fcnchen",
27 | "0509": "Arcisstr. 21, 80333 M\u00fcnchen",
28 | "0510": "Arcisstr. 21, 80333 M\u00fcnchen",
29 | "0511": "Arcisstr. 21, 80333 M\u00fcnchen",
30 | "0512": "Arcisstr. 21, 80333 M\u00fcnchen",
31 | "1501": "Ismaninger Stra\u00dfe 22, 81675 M\u00fcnchen",
32 | "1503": "Ismaningerstr. 22, 81675 M\u00fcnchen",
33 | "1514": "Trogerstr. 9, 81675 M\u00fcnchen",
34 | "1523": "Ismaningerstr. 22, 81675 M\u00fcnchen",
35 | "1531": "Trogerstr.4/Einsteinstr.65 4/65, 81675 M\u00fcnchen",
36 | "1533": "Trogerstr. 8, 81675 M\u00fcnchen",
37 | "1535": "Trogerstr. 12, 81675 M\u00fcnchen",
38 | "1536": "Trogerstr. 14, 81675 M\u00fcnchen",
39 | "1538": "Trogerstr. 18, 81675 M\u00fcnchen",
40 | "1545": "Ismaningerstr. 22, 81675 M\u00fcnchen",
41 | "1548": "Schneckenburgerstr. 8, 81675 M\u00fcnchen",
42 | "1551": "Ismaninger Stra\u00dfe 22, 81675 M\u00fcnchen",
43 | "1559": "Trogerstr. 30, 81675 M\u00fcnchen",
44 | "1601": "Biedersteiner Str. 29, 80802 M\u00fcnchen",
45 | "1602": "Biedersteiner Str. 29, 80802 M\u00fcnchen",
46 | "1603": "Biedersteiner Str. 29, 80802 M\u00fcnchen",
47 | "1607": "Dietlindenstr./Biedersteiner Str. 29, 80802 M\u00fcnchen",
48 | "1608": "Dietlindenstr./Biedersteiner Str. 29, 80802 M\u00fcnchen",
49 | "1650": "Dietlindenstr./Biedersteiner Str. 29, 80802 M\u00fcnchen",
50 | "1652": "Dietlindenstr./Biedersteiner Str. 29, 80802 M\u00fcnchen",
51 | "1713": "Nigerstr. 3, 81675 M\u00fcnchen",
52 | "1725": "Orleanstr. 47, 81667 M\u00fcnchen",
53 | "2103": "Schragenhofstr. 31, 80992 M\u00fcnchen",
54 | "2104": "Schragenhofstr. 31, 80992 M\u00fcnchen",
55 | "2105": "Schragenhofstr. 31, 80992 M\u00fcnchen",
56 | "2106": "Schragenhofstr. 31, 80992 M\u00fcnchen",
57 | "2107": "Schragenhofstr. 31, 80992 M\u00fcnchen",
58 | "2108": "Schragenhofstr. 31, 80992 M\u00fcnchen",
59 | "2109": "Schragenhofstr. 31, 80992 M\u00fcnchen",
60 | "2301": "Connollystr. 32, 80809 M\u00fcnchen",
61 | "2302": "Connollystr. 32, 80809 M\u00fcnchen",
62 | "2303": "Connollystr. 32, 80809 M\u00fcnchen",
63 | "2304": "Connollystr. 32, 80809 M\u00fcnchen",
64 | "2305": "Connollystr. 32, 80809 M\u00fcnchen",
65 | "2308": "Connollystr. 32, 80809 M\u00fcnchen",
66 | "2309": "Connollystr. 32, 80809 M\u00fcnchen",
67 | "2310": "Connollystr. 32, 80809 M\u00fcnchen",
68 | "2311": "Connollystr. 32, 80809 M\u00fcnchen",
69 | "2312": "Connollystr. 32, 80809 M\u00fcnchen",
70 | "2313": "Connollystr. 32, 80809 M\u00fcnchen",
71 | "2315": "Connollystr. 32, 80809 M\u00fcnchen",
72 | "2321": "Connollystr. 32, 80809 M\u00fcnchen",
73 | "2350": "Spiridon-Louis-Ring, 80809 M\u00fcnchen",
74 | "2351": "Olympiapark M\u00fcnchen, 80809 M\u00fcnchen",
75 | "2352": "Olympiapark M\u00fcnchen, 80809 M\u00fcnchen",
76 | "2353": "Staudingerstr., 81737 M\u00fcnchen",
77 | "2354": "Regattaanlage Oberschlei\u00dfheim, 85764 Oberschlei\u00dfheim",
78 | "2361": "Connollystra\u00dfe 32, 80809 M\u00fcnchen",
79 | "2401": "Winzererstr. 45, 80797 M\u00fcnchen",
80 | "2402": "Winzererstr. 45, 80797 M\u00fcnchen",
81 | "2410": "He\u00dfstr. 134, 80797 M\u00fcnchen",
82 | "2522": "Ismanninger Str. 22, 81675 M\u00fcnchen",
83 | "2601": "Baumbachstr. 7, 81245 M\u00fcnchen",
84 | "2602": "Baumbachstr. 7, 81245 M\u00fcnchen",
85 | "2604": "Baumbachstr. 7, 81245 M\u00fcnchen",
86 | "2605": "Baumbachstr. 7, 81245 M\u00fcnchen",
87 | "2607": "Baumbachstr. 7, 81245 M\u00fcnchen",
88 | "2701": "Marchioninistr. 17, 81377 M\u00fcnchen",
89 | "2801": "Marchioninistr. 17, 81377 M\u00fcnchen",
90 | "2804": "Museumsinsel 1, 80538 M\u00fcnchen",
91 | "2805": "Karl-Benz-Str. 15, 85221 Dachau",
92 | "2806": "Oettingenstra\u00dfe 15, 80538 M\u00fcnchen",
93 | "2807": "Barerstr. 40, 80799 M\u00fcnchen",
94 | "2808": "Dr. Albert-Frank-Str. 32, 83308 Trostberg",
95 | "2809": "Praterinsel 2, 80538 M\u00fcnchen",
96 | "2903": "Augustenstr. 44_46, 80333 M\u00fcnchen",
97 | "2906": "Karlstra\u00dfe 45-47, 80333 M\u00fcnchen",
98 | "2907": "Marsstr. 20-22, 80335 M\u00fcnchen",
99 | "2908": "Marsstra\u00dfe 40, 80335 M\u00fcnchen",
100 | "2909": "Denisstr. 1B, 80333 M\u00fcnchen",
101 | "2910": "Richard-Wagner-Str. 1, 80333 M\u00fcnchen",
102 | "2911": "Richard-Wagner-Str. 3, 80333 M\u00fcnchen",
103 | "2926": "Leopoldstr. 139/145, 80804 M\u00fcnchen",
104 | "2927": "Petersgasse 18, 94315 Straubing",
105 | "2929": "Schulgasse 20, 94315 Straubing",
106 | "2940": "Georg-Brauchle-Ring 50_66, 80992 M\u00fcnchen",
107 | "2941": "Georg-Brauchle-Ring 60_62, 80992 M\u00fcnchen",
108 | "3035": "Ingolst\u00e4dter Landstra\u00dfe 1, 85764 Oberschlei\u00dfheim",
109 | "3100": "Am Burghof 3, 99947 M\u00fclverstedt",
110 | "3101": "Obernach/Walchensee, 82432 Obernach",
111 | "3102": "Obernach/Walchensee, 82432 Obernach",
112 | "3103": "Obernach/Walchensee, 82432 Obernach",
113 | "3104": "Obernach/Walchensee, 82432 Obernach",
114 | "3105": "Obernach/Walchensee, 82432 Obernach",
115 | "3106": "Obernach/Walchensee, 82432 Obernach",
116 | "3107": "Obernach/Walchensee, 82432 Obernach",
117 | "3108": "Obernach/Walchensee, 82432 Obernach",
118 | "3109": "Obernach/Walchensee, 82432 Obernach",
119 | "3110": "Obernach/Walchensee, 82432 Obernach",
120 | "3111": "Obernach/Walchensee, 82432 Obernach",
121 | "3112": "Obernach/Walchensee, 82432 Obernach",
122 | "3113": "Obernach/Walchensee, 82432 Obernach",
123 | "3114": "Obernach/Walchensee, 82432 Obernach",
124 | "3115": "Obernach/Walchensee, 82432 Obernach",
125 | "3116": "Obernach/Walchensee, 82432 Obernach",
126 | "3117": "Obernach/Walchensee, 82432 Obernach",
127 | "3118": "Obernach/Walchensee, 82432 Obernach",
128 | "3119": "Obernach/Walchensee, 82432 Obernach",
129 | "3120": "Obernach/Walchensee, 82432 Obernach",
130 | "3121": "Obernach/Walchensee, 82432 Obernach",
131 | "3201": "Lindenweg 15, 82223 Eichenau",
132 | "3401": "Alfons-Goppel-Stra\u00dfe 11, 80333 M\u00fcnchen",
133 | "3402": "Alfons-Goppel-Stra\u00dfe 11, 80333 M\u00fcnchen",
134 | "3501": "Schulgasse 16, 94315 Straubing",
135 | "3502": "Schulgasse 22, 94315 Straubing",
136 | "3503": "Petersgasse 5, 94315 Straubing",
137 | "3901": "Unterer Seeweg 5, 82319 Starnberg",
138 | "3902": "Unterer Seeweg 5, 82319 Starnberg",
139 | "3904": "Unterer Seeweg 5, 82319 Starnberg",
140 | "4001": "Oberer Schlangenweg, 85354 Freising",
141 | "4101": "Alte Akademie 1, 85354 Freising",
142 | "4102": "Alte Akademie 8, 85354 Freising",
143 | "4103": "Alte Akademie 8a, 85354 Freising",
144 | "4105": "Alte Akademie 12, 85354 Freising",
145 | "4106": "Alte Akademie 14, 85354 Freising",
146 | "4107": "Alte Akademie 16, 85354 Freising",
147 | "4108": "Alte Akademie 10, 85354 Freising",
148 | "4109": "Weihenstephaner Steig 22, 85354 Freising",
149 | "4110": "Weihenstephaner Steig 20, 85354 Freising",
150 | "4111": "Weihenstephaner Steig 18, 85354 Freising",
151 | "4113": "Weihenstephaner Steig 16, 85354 Freising",
152 | "4114": "Weihenstephaner Steig 14, 85354 Freising",
153 | "4115": "Weihenstephaner Steig 19, 85350 Freising",
154 | "4116": "Weihenstephaner Steig 17, 85354 Freising",
155 | "4117": "Weihenstephaner Steig ?, 85354 Freising",
156 | "4119": "Hohenbachernstr. 15, 85354 Freising",
157 | "4120": "Hohenbachernstr. 17, 85354 Freising",
158 | "4124": "Weihenstephaner Berg 3, 85354 Freising",
159 | "4126": "Weihenstephaner Berg 1, 85354 Freising",
160 | "4128": "Am Hofgarten, 85354 Freising",
161 | "4129": "M\u00fchlenweg 22, 85354 Freising",
162 | "4130": "Weihenstephaner Berg 13, 85354 Freising",
163 | "4131": "M\u00fchlenweg 22, 85354 Freising",
164 | "4132": "M\u00fchlenweg 18, 85354 Freising",
165 | "4153": "Weihenstephaner Berg 13, 85354 Freising",
166 | "4155": "Weihenstephaner Berg, 85354 Freising",
167 | "4156": "Weihenstephaner Berg 21, 85354 Freising",
168 | "4180": "Staatsgut Veitshof, 85354 Freising",
169 | "4181": "Staatsgut Veitshof, 85354 Freising",
170 | "4182": "Staatsgut Veitshof, 85354 Freising",
171 | "4183": "Staatsgut Veitshof, 85354 Freising",
172 | "4184": "Staatsgut Veitshof, 85354 Freising",
173 | "4185": "Staatsgut Veitshof, 85354 Freising",
174 | "4187": "Staatsgut Veitshof, 85354 Freising",
175 | "4188": "Staatsgut Veitshof, 85354 Freising",
176 | "4189": "Staatsgut Veitshof, 85354 Freising",
177 | "4190": "Staatsgut Veitshof, 85354 Freising",
178 | "4191": "Staatsgut Veitshof, 85354 Freising",
179 | "4192": "Staatsgut Veitshof, 85354 Freising",
180 | "4202": "Liesel-Beckmann-Str., 85354 Freising",
181 | "4205": "Hohenbachernstr., 85354 Freising",
182 | "4210": "Am Staudengarten 2, 85354 Freising",
183 | "4211": "V\u00f6ttinger Stra\u00dfe 36, 85354 Freising",
184 | "4212": "Emil-Erlenmeyer-Forum 5, 85354 Freising",
185 | "4213": "Maximus-von-Imhof-Forum 2, 85354 Freising",
186 | "4214": "Maximus-von-Imhof-Forum 6, 85354 Freising",
187 | "4215": "Maximus-von-Imhof-Forum 4, 85354 Freising",
188 | "4216": "Maximus-von-Imhof-Forum 5, 85354 Freising",
189 | "4217": "Emil-Ramann-Str. 2, 85354 Freising",
190 | "4218": "Emil-Ramann-Str. 4, 85354 Freising",
191 | "4219": "Emil-Ramann-Str. 6, 85354 Freising",
192 | "4220": "Maximus-von-Imhof-Forum 1+3, 85354 Freising",
193 | "4221": "Emil-Erlenmeyer-Forum 7, 85354 Freising",
194 | "4223": "Emil-Ramann-Str. 8, 85354 Freising",
195 | "4224": "Gregor-Mendel-Str. 2, 85354 Freising",
196 | "4225": "Lise-Meitner-Str. 34, 85354 Freising",
197 | "4226": "Gregor-Mendel-Str. 4, 85354 Freising",
198 | "4227": "Hans-Carl-von-Carlowitz-Platz 3, 85354 Freising",
199 | "4230": "Staatsgut Weihenstephan, 85354 Freising",
200 | "4231": "D\u00fcrnast I, 85354 Freising",
201 | "4232": "D\u00fcrnast II, 85354 Freising",
202 | "4234": "D\u00fcrnast III, 85354 Freising",
203 | "4235": "D\u00fcrnast IV, 85354 Freising",
204 | "4236": "Staatsgut Weihenstephan, 85354 Freising",
205 | "4237": "Staatsgut Weihenstephan, 85354 Freising",
206 | "4238": "Emil-Erlenmeyer-Forum 2, 85354 Freising",
207 | "4239": "Emil-Erlenmeyer-Forum 3, 85354 Freising",
208 | "4254": "Emil-Erlenmeyer-Forum 6, 85354 Freising",
209 | "4259": "Emil-Erlenmeyer-Forum 8, 85354 Freising",
210 | "4264": "Emil-Erlenmeyer-Forum 8, 85354 Freising",
211 | "4267": "Emil-Erlenmeyer-Forum 6, 85354 Freising",
212 | "4275": "An der M\u00fchle 20, 85354 Freising",
213 | "4277": "Hans-Carl-von-Carlowitz-Platz 2, 85354 Freising",
214 | "4278": "Hans-Carl-von-Carlowitz-Platz 1, 85354 Freising",
215 | "4281": "Am Staudengarten 1, 85354 Freising",
216 | "4299": "Hohenbachernstra\u00dfe 9, 85354 Freising",
217 | "4303": "Lange Point 51, 85354 Freising",
218 | "4304": "Lange Point 51, 85354 Freising",
219 | "4307": "Liesel-Beckmann-Str. 6, 85354 Freising",
220 | "4308": "Liesel-Beckmann-Str. 4, 85354 Freising",
221 | "4309": "Blumenstr. 16, 85354 Freising",
222 | "4310": "Blumenstr. 16, 85354 Freising",
223 | "4311": "Blumenstr. 16, 85354 Freising",
224 | "4314": "Am Staudengarten, 85354 Freising",
225 | "4315": "Lange Point 51, 85354 Freising",
226 | "4317": "Liesel-Beckmann-Str. 1, 85354 Freising",
227 | "4318": "Liesel-Beckmann-Str. 2, 85354 Freising",
228 | "4319": "Liesel-Beckmann-Stra\u00dfe, 85354 Freising",
229 | "4321": "Lange Point 24, 85354 Freising",
230 | "4322": "Lange Point 24, 85354 Freising",
231 | "4323": "Lange Point 24, 85354 Freising",
232 | "4324": "Lange Point 20, 85354 Freising",
233 | "4353": "Lange Point 4, 85354 Freising",
234 | "4355": "Zur Kreutzbreite 4, 85354 Freising",
235 | "4361": "Am Gereuth 5, 85354 Freising",
236 | "4362": "Lange Point 10, 85354 Freising",
237 | "4368": "Am Gereuth 6, 85354 Freising",
238 | "4387": "Wippenhauserstr. 51, 85354 Freising",
239 | "4401": "Hofmark 3, 82393 Iffeldorf",
240 | "4402": "Hofmark 3, 82393 Iffeldorf",
241 | "4403": "Hofmark 3, 82393 Iffeldorf",
242 | "4404": "Hofmark 3, 82393 Iffeldorf",
243 | "4405": "Hofmark 3, 82393 Iffeldorf",
244 | "4501": "Staatsgut Weihenstephan, 85354 Freising",
245 | "4502": "Staatsgut Weihenstephan, 85354 Freising",
246 | "4503": "Staatsgut Weihenstephan, 85354 Freising",
247 | "4505": "Staatsgut Weihenstephan, 85354 Freising",
248 | "4506": "Staatsgut Weihenstephan, 85354 Freising",
249 | "4508": "Staatsgut Weihenstephan, 85354 Freising",
250 | "4509": "Staatsgut Weihenstephan, 85354 Freising",
251 | "4510": "Staatsgut Weihenstephan, 85354 Freising",
252 | "4512": "Staatsgut Weihenstephan, 85354 Freising",
253 | "4513": "Staatsgut Weihenstephan, 85354 Freising",
254 | "4521": "Viehhausen 4, 85402 Kranzberg",
255 | "4522": "Viehhausen 4, 85402 Kranzberg",
256 | "4523": "Viehhausen 4, 85402 Kranzberg",
257 | "4524": "Viehhausen 4, 85402 Kranzberg",
258 | "4601": "Staatsgut Thalhausen, 85402 Kranzberg",
259 | "4602": "Staatsgut Thalhausen, 85402 Kranzberg",
260 | "4603": "Staatsgut Thalhausen, 85402 Kranzberg",
261 | "4604": "Staatsgut Thalhausen, 85402 Kranzberg",
262 | "4605": "Staatsgut Thalhausen, 85402 Kranzberg",
263 | "4606": "Staatsgut Thalhausen, 85402 Kranzberg",
264 | "4607": "Staatsgut Thalhausen, 85402 Kranzberg",
265 | "4608": "Staatsgut Thalhausen, 85402 Kranzberg",
266 | "4609": "Staatsgut Thalhausen, 85402 Kranzberg",
267 | "4610": "Staatsgut Thalhausen, 85402 Kranzberg",
268 | "4611": "Staatsgut Thalhausen, 85402 Kranzberg",
269 | "4612": "Staatsgut Thalhausen, 85402 Kranzberg",
270 | "4613": "Staatsgut Thalhausen, 85402 Kranzberg",
271 | "4614": "Staatsgut Thalhausen, 85402 Kranzberg",
272 | "4615": "Staatsgut Thalhausen, 85402 Kranzberg",
273 | "4616": "Staatsgut Thalhausen, 85402 Kranzberg",
274 | "4620": "Staatsgut Thalhausen, 85402 Kranzberg",
275 | "4801": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
276 | "4802": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
277 | "4803": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
278 | "4804": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
279 | "4805": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
280 | "4806": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
281 | "4807": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
282 | "4808": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
283 | "4809": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
284 | "4810": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
285 | "4811": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
286 | "4812": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
287 | "4813": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
288 | "4814": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
289 | "4815": "Staatsgut Gr\u00fcnschwaige, 85462 Eitting",
290 | "4901": "Staatsgut Roggenstein, 82223 Eichenau",
291 | "4902": "Staatsgut Roggenstein, 82223 Eichenau",
292 | "4903": "Staatsgut Roggenstein, 82223 Eichenau",
293 | "4907": "Staatsgut Roggenstein, 82223 Eichenau",
294 | "4908": "Staatsgut Roggenstein, 82223 Eichenau",
295 | "4909": "Staatsgut Roggenstein, 82223 Eichenau",
296 | "4910": "Staatsgut Roggenstein, 82223 Eichenau",
297 | "4914": "Staatsgut Roggenstein, 82223 Eichenau",
298 | "4915": "Staatsgut Roggenstein, 82223 Eichenau",
299 | "4916": "Staatsgut Roggenstein, 82223 Eichenau",
300 | "4920": "Staatsgut Roggenstein, 82223 Eichenau",
301 | "5101": "James-Franck-Str. 1, 85748 Garching b. M\u00fcnchen",
302 | "5103": "Boltzmannstr. 10, 85748 Garching b. M\u00fcnchen",
303 | "5104": "Boltzmannstr. 16, 85748 Garching b. M\u00fcnchen",
304 | "5105": "Boltzmannstr. 12, 85748 Garching b. M\u00fcnchen",
305 | "5107": "Am Coulombwall 2, 85748 Garching b. M\u00fcnchen",
306 | "5108": "James-Franck-Str. 1, 85748 Garching b. M\u00fcnchen",
307 | "5109": "Am Coulombwall 1, 85748 Garching b. M\u00fcnchen",
308 | "5110": "James-Franck-Str. 1, 85748 Garching b. M\u00fcnchen",
309 | "5111": "Am Coulombwall 3, 85748 Garching b. M\u00fcnchen",
310 | "5112": "Am Coulombwall 4, 85748 Garching b. M\u00fcnchen",
311 | "5115": "Am Coulombwall 4a, 85748 Garching b. M\u00fcnchen",
312 | "5116": "Am Coulombwall 4a, 85748 Garching b. M\u00fcnchen",
313 | "5120": "Am Coulombwall 6, 85748 Garching b. M\u00fcnchen",
314 | "5121": "Am Coulombwall 6, 85748 Garching b. M\u00fcnchen",
315 | "5122": "Am Coulombwall 6, 85748 Garching b. M\u00fcnchen",
316 | "5123": "Am Coulombwall 1, 85748 Garching b. M\u00fcnchen",
317 | "5124": "James-Franck-Strasse 1, 85748 Garching b. M\u00fcnchen",
318 | "5125": "Am Coloumbwall 1a, 85748 Garching b. M\u00fcnchen",
319 | "5126": "Am Coloumbwall 1b, 85748 Garching b. M\u00fcnchen",
320 | "5130": "Boltzmannstra\u00dfe 10a, 85748 Garching b. M\u00fcnchen",
321 | "5131": "Boltzmannstra\u00dfe 10b, 85748 Garching b. M\u00fcnchen",
322 | "5140": "James-Frank-Str. 1, 85748 Garching b. M\u00fcnchen",
323 | "5160": "Am Coulombwall 6, 85748 Garching b. M\u00fcnchen",
324 | "5202": "Lichtenbergstr. 1, 85748 Garching b. M\u00fcnchen",
325 | "5203": "Walther-Mei\u00dfner-Str. 1, 85748 Garching b. M\u00fcnchen",
326 | "5204": "Walther-Mei\u00dfner-Str. 4, 85748 Garching b. M\u00fcnchen",
327 | "5212": "Walther-Mei\u00dfner-Str. 3, 85748 Garching b. M\u00fcnchen",
328 | "5219": "Walther-Mei\u00dfner-Str. 2, 85748 Garching b. M\u00fcnchen",
329 | "5251": "Walther-Mei\u00dfner-Str. 3, 85748 Garching b. M\u00fcnchen",
330 | "5252": "Walther-Mei\u00dfner-Str. 3, 85748 Garching b. M\u00fcnchen",
331 | "5268": "Walther-Mei\u00dfner-Str. 3, 85748 Garching b. M\u00fcnchen",
332 | "5269": "Walther-Mei\u00dfner-Str. 3, 85748 Garching b. M\u00fcnchen",
333 | "5301": "Lichtenbergstra\u00dfe 2a, 85748 Garching b. M\u00fcnchen",
334 | "5302": "Lichtenbergstr. 2, 85748 Garching b. M\u00fcnchen",
335 | "5401": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
336 | "5402": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
337 | "5403": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
338 | "5404": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
339 | "5406": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
340 | "5407": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
341 | "5408": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
342 | "5409": "Lichtenbergstr. 4, 85748 Garching b. M\u00fcnchen",
343 | "5410": "Ernst-Otto-Fischer-Stra\u00dfe 1, 85748 Garching b. M\u00fcnchen",
344 | "5413": "Ernst-Otto-Fischer-Stra\u00dfe 2, 85748 Garching b. M\u00fcnchen",
345 | "5414": "Lichtenbergstr. 4a, 85748 Garching b. M\u00fcnchen",
346 | "5433": "Lichtenbergstr. 6, 85748 Garching b. M\u00fcnchen",
347 | "5501": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
348 | "5502": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
349 | "5503": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
350 | "5504": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
351 | "5505": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
352 | "5506": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
353 | "5507": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
354 | "5508": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
355 | "5510": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
356 | "5513": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
357 | "5514": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
358 | "5515": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
359 | "5517": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
360 | "5518": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
361 | "5519": "Boltzmannstr. 15, 85748 Garching b. M\u00fcnchen",
362 | "5530": "Boltzmannstr. 17, 85748 Garching b. M\u00fcnchen",
363 | "5531": "Lichtenbergstra\u00dfe 9, 85748 Garching b. M\u00fcnchen",
364 | "5601": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
365 | "5602": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
366 | "5603": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
367 | "5604": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
368 | "5605": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
369 | "5606": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
370 | "5607": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
371 | "5608": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
372 | "5609": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
373 | "5610": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
374 | "5611": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
375 | "5612": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
376 | "5613": "Boltzmannstr. 3, 85748 Garching b. M\u00fcnchen",
377 | "5620": "Boltzmannstr. 5, 85748 Garching b. M\u00fcnchen",
378 | "5622": "Boltzmannstr. 5, 85748 Garching b. M\u00fcnchen",
379 | "5701": "Boltzmannstr. 11, 85748 Garching b. M\u00fcnchen",
380 | "6101": "R\u00f6merhofweg 67, 85748 Garching b. M\u00fcnchen",
381 | "6102": "R\u00f6merhofweg 67, 85748 Garching b. M\u00fcnchen",
382 | "6103": "R\u00f6merhofweg 67, 85748 Garching b. M\u00fcnchen",
383 | "6107": "R\u00f6merhofweg 67, 85748 Garching b. M\u00fcnchen",
384 | "6202": "R\u00f6merhofweg 67, 85748 Garching b. M\u00fcnchen",
385 | "7910": "Ludwig-Prandtl-Stra\u00dfe 1, 85748 Garching b. M\u00fcnchen",
386 | "8101": "Parkring 11-13, 85748 Garching b. M\u00fcnchen",
387 | "8102": "Parkring 35-39, 85748 Garching b. M\u00fcnchen",
388 | "8111": "Schlei\u00dfheimerstra\u00dfe 90a, 85748 Garching b. M\u00fcnchen",
389 | "8120": "Walther-von-Dyck-Strasse 10, 85748 Garching b. M\u00fcnchen",
390 | "8121": "Walther-von-Dyck-Strasse 12, 85748 Garching b. M\u00fcnchen",
391 | "9001": "Raitenhaslach 11, 84489 Burghausen",
392 | "9377": "Lise-Meitner-Stra\u00dfe 9-11, 85521 Ottobrunn"
393 | }
394 |
--------------------------------------------------------------------------------