From f49de741bba274bd69f6a553ae5b8fddb3bad318 Mon Sep 17 00:00:00 2001 From: Pascal Syma Date: Sat, 24 Feb 2024 00:51:57 +0100 Subject: [PATCH] Add data --- index.html | 29 ++++++- package-lock.json | 31 ++++++++ package.json | 7 +- src/counter.ts | 9 --- src/main.ts | 191 ++++++++++++++++++++++++++++++++++++++++----- src/overpass.ts | 59 ++++++++++++++ src/style.css | 11 +++ src/typescript.svg | 1 - 8 files changed, 302 insertions(+), 36 deletions(-) delete mode 100644 src/counter.ts create mode 100644 src/overpass.ts delete mode 100644 src/typescript.svg diff --git a/index.html b/index.html index 963445f..b857d53 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,35 @@ - + - Vite + TS + Konsumverbot Cannabis + + + + + + + + + + + -

aaa

-
+
+
+
diff --git a/package-lock.json b/package-lock.json index 846ae5c..f29fcc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,11 @@ "": { "name": "weedmap", "version": "0.0.0", + "dependencies": { + "@types/leaflet": "^1.9.3", + "leaflet": "^1.9.4", + "leaflet-easybutton": "^2.4.0" + }, "devDependencies": { "autoprefixer": "^10.4.17", "postcss": "^8.4.35", @@ -490,6 +495,19 @@ "node": ">=14" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, + "node_modules/@types/leaflet": { + "version": "1.9.8", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.8.tgz", + "integrity": "sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -1081,6 +1099,19 @@ "jiti": "bin/jiti.js" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-easybutton": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/leaflet-easybutton/-/leaflet-easybutton-2.4.0.tgz", + "integrity": "sha512-O+qsQq4zTF6ds8VClnytobTH/MKalctlPpiA8L+bNKHP14J3lgJpvEd/jSpq9mHTI6qOzRAvbQX6wS6qNwThvg==", + "dependencies": { + "leaflet": "^1.0.1" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", diff --git a/package.json b/package.json index e756f94..0634862 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,10 @@ "tailwindcss": "^3.4.1", "typescript": "^5.0.2", "vite": "^4.4.5" + }, + "dependencies": { + "@types/leaflet": "^1.9.3", + "leaflet": "^1.9.4", + "leaflet-easybutton": "^2.4.0" } -} +} \ No newline at end of file diff --git a/src/counter.ts b/src/counter.ts deleted file mode 100644 index da18bde..0000000 --- a/src/counter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function setupCounter(element: HTMLButtonElement) { - let counter = 0; - const setCounter = (count: number) => { - counter = count; - element.innerHTML = `count is ${counter}`; - }; - element.addEventListener('click', () => setCounter(counter + 1)); - setCounter(0); -} diff --git a/src/main.ts b/src/main.ts index 05d8a95..6274983 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,24 +1,173 @@ import './style.css'; -import typescriptLogo from './typescript.svg'; -import viteLogo from '/vite.svg'; -import { setupCounter } from './counter.ts'; +import 'leaflet/dist/leaflet.css'; +import { + Circle, + Control, + easyButton, + FeatureGroup, + Map as LMap, + Popup, + TileLayer +} from 'leaflet'; +import 'leaflet-easybutton'; +import { BannedArea, BannedAreas, NWRElement, tagsToHTML } from './overpass.ts'; -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

-
-`; +const m_mono = new TileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + { + attribution: + 'Kartendaten © OpenStreetMap Mitwirkende' + } +); -setupCounter(document.querySelector('#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