('#counter')!);
+const features: { [reason in BannedArea]: FeatureGroup } = {
+ [BannedAreas.SCHOOL]: new FeatureGroup(),
+ [BannedAreas.UNIVERSITY]: new FeatureGroup(),
+ [BannedAreas.SPORT]: new FeatureGroup(),
+ [BannedAreas.PEDESTRIAN]: new FeatureGroup(),
+ [BannedAreas.OTHER]: new FeatureGroup()
+};
+
+const center = [53.0711829, 8.8087718] as [number, number];
+const map = new LMap('map', {
+ center,
+ zoom: 14,
+ zoomControl: true,
+ layers: [m_mono, ...Object.values(features)]
+});
+
+new Popup()
+ .setLatLng(center)
+ .setContent(
+ `Diese Karte zeigt die Orte, an denen der Konsum von Cannabis verboten ist. Um jeden Ort wird ein Kreis mit Radius von 200 Metern (gemäß Gesetzentwurf § 5 Konsumverbot ) gezeichnet.
+Oben rechts können die verschiedenen Arten von Orten an- und abgeschaltet werden
+Datenquelle: OpenStreetMaps via Overpass
+Keine Garantie für Korrektheit!
`
+ )
+ .addTo(map);
+
+map.on('moveend', async () => {
+ await fetchData();
+});
+let loading = false;
+async function fetchData() {
+ if (map.getZoom() < 12 || loading) {
+ return;
+ }
+ loading = true;
+ const bounds = map.getBounds();
+ const query = `[out:json][timeout:25][bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}];
+(
+ nwr["amenity"="school"];
+ nwr["building"="school"];
+ nwr["building"="university"];
+ nwr["leisure"="playground"];
+ nwr["amenity"="kindergarten"];
+ nwr["community_centre"="youth_centre"];
+ nwr["leisure"="sports_centre"];
+ nwr["leisure"="sports_hall"];
+ nwr["leisure"="stadium"];
+ nwr["leisure"="track"];
+ nwr["leisure"="pitch"];
+ nwr["sport"]["amenity"!="restaurant"]["amenity"!="pub"]["amenity"!="cafe"]["tourism"!="hotel"];
+ nwr["highway"="pedestrian"]["area"!="yes"];
+);
+out geom;`;
+
+ const elements = await fetch('https://overpass-api.de/api/interpreter', {
+ headers: {
+ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
+ },
+ body: 'data=' + encodeURIComponent(query),
+ method: 'POST'
+ })
+ .then((b) => b.json())
+ .catch(() => ({ elements: [] }))
+ .then((r) => r.elements as NWRElement[]);
+
+ Object.values(features).forEach((f) => f.clearLayers());
+
+ elements.forEach((e) => {
+ const reason = getReason(e);
+ const marker = new Circle(
+ e.type === 'node' ? [e.lat, e.lon] : [e.bounds.maxlat, e.bounds.maxlon],
+ 200,
+ {
+ radius: 200,
+ stroke: false,
+ fillOpacity: 0.3,
+ fillColor: colorMap[reason]
+ }
+ ).bindPopup(
+ `${tagsToHTML({ reason, ...e.tags })}
`,
+ { maxWidth: 500 }
+ );
+
+ features[reason].addLayer(marker);
+ });
+
+ loading = false;
+}
+
+const colorMap: { [reason in BannedArea]: string } = {
+ [BannedAreas.SCHOOL]: 'red',
+ [BannedAreas.UNIVERSITY]: 'orange',
+ [BannedAreas.SPORT]: 'yellow',
+ [BannedAreas.PEDESTRIAN]: 'green',
+ [BannedAreas.OTHER]: 'blue'
+};
+
+function getReason(element: NWRElement): BannedArea {
+ const t = element.tags;
+ if (
+ t.amenity === 'school' ||
+ t.building === 'school' ||
+ t.amenity === 'kindergarten' ||
+ t.leisure === 'playground' ||
+ t.community_centre === 'youth_centre'
+ ) {
+ return BannedAreas.SCHOOL;
+ }
+ if (t.building === 'university') {
+ return BannedAreas.UNIVERSITY;
+ }
+ if (
+ t.sport !== undefined ||
+ ['sports_centre', 'sports_hall', 'stadium', 'track', 'pitch'].includes(
+ t.leisure
+ )
+ ) {
+ return BannedAreas.SPORT;
+ }
+ if (t.highway !== undefined) {
+ return BannedAreas.PEDESTRIAN;
+ }
+
+ return BannedAreas.OTHER;
+}
+
+new Control.Layers(
+ {},
+ Object.fromEntries(
+ Object.entries(features).map(([k, v]) => [
+ ` ${k}`,
+ v
+ ])
+ )
+).addTo(map);
+
+easyButton(
+ '📍 ',
+ function () {
+ if (!navigator.geolocation) {
+ return;
+ }
+ navigator.geolocation.getCurrentPosition(
+ (p) => map.panTo([p.coords.latitude, p.coords.longitude]),
+ undefined,
+ { enableHighAccuracy: true }
+ );
+ }
+).addTo(map);
+
+fetchData();
diff --git a/src/overpass.ts b/src/overpass.ts
new file mode 100644
index 0000000..ba3e56a
--- /dev/null
+++ b/src/overpass.ts
@@ -0,0 +1,59 @@
+export interface APIElement {
+ id: string;
+ tags: Record;
+}
+
+export interface NodeElement extends APIElement {
+ type: 'node';
+ lat: number;
+ lon: number;
+}
+
+export interface WayElement extends APIElement {
+ type: 'way';
+ bounds: {
+ minlat: number;
+ minlon: number;
+ maxlat: number;
+ maxlon: number;
+ };
+ nodes: number[];
+ geometry: { lat: number; lon: number }[];
+}
+
+export interface RelationElement extends APIElement {
+ type: 'relation';
+ bounds: {
+ minlat: number;
+ minlon: number;
+ maxlat: number;
+ maxlon: number;
+ };
+ members: {
+ type: 'way';
+ ref: number;
+ role: string;
+ geometry: { lat: number; lon: number }[];
+ }[];
+}
+
+export type NWRElement = Readonly;
+
+export function tagsToHTML(tags: APIElement['tags']) {
+ const entries = Object.entries(tags);
+
+ let keyValues = entries.map(
+ ([k, v]) => `${k}: ${v} `
+ );
+ return `${keyValues.join('')}
`;
+}
+
+export const BannedAreas = {
+ SCHOOL: 'Schule, Kindergarten, Spielplätze, Jugendeinrichtungen',
+ UNIVERSITY: 'Hochschule',
+ SPORT: 'Sportanlagen',
+ PEDESTRIAN: 'Fußgängerzonen (7-20 Uhr)',
+ OTHER: 'Andere'
+} as const;
+
+export type BannedArea = (typeof BannedAreas)[keyof typeof BannedAreas];
diff --git a/src/style.css b/src/style.css
index b5c61c9..e3277e6 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,3 +1,14 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+html,
+body {
+ height: 100%;
+ padding: 0;
+ margin: 0;
+}
+
+#map {
+ z-index: 0;
+}
diff --git a/src/typescript.svg b/src/typescript.svg
deleted file mode 100644
index d91c910..0000000
--- a/src/typescript.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file