Add data
This commit is contained in:
+25
-4
@@ -1,14 +1,35 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + TS</title>
|
<title>Konsumverbot Cannabis</title>
|
||||||
|
<link rel="canonical" href="https://weed.syma.dev/" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Diese Karte zeigt die Orte, an denen der Konsum von Cannabis verboten ist."
|
||||||
|
/>
|
||||||
|
<meta property="og:title" content="Konsumverbot Cannabis" />
|
||||||
|
<meta property="og:locale" content="de_DE" />
|
||||||
|
<meta
|
||||||
|
name="og:description"
|
||||||
|
content="Diese Karte zeigt die Orte, an denen der Konsum von Cannabis verboten ist."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://weed.syma.dev/" />
|
||||||
|
<meta name="twitter:title" content="Konsumverbot Cannabis " />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Diese Karte zeigt die Orte, an denen der Konsum von Cannabis verboten ist."
|
||||||
|
/>
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:site" content="@passidel" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p class="text-3xl font-bold underline">aaa</p>
|
<div id="app">
|
||||||
<div id="app"></div>
|
<div id="map" class="w-screen h-screen"></div>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+31
@@ -7,6 +7,11 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "weedmap",
|
"name": "weedmap",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/leaflet": "^1.9.3",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet-easybutton": "^2.4.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
@@ -490,6 +495,19 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
@@ -1081,6 +1099,19 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
|
|||||||
+6
-1
@@ -17,5 +17,10 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.5"
|
"vite": "^4.4.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/leaflet": "^1.9.3",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet-easybutton": "^2.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
+170
-21
@@ -1,24 +1,173 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import typescriptLogo from './typescript.svg';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import viteLogo from '/vite.svg';
|
import {
|
||||||
import { setupCounter } from './counter.ts';
|
Circle,
|
||||||
|
Control,
|
||||||
|
easyButton,
|
||||||
|
FeatureGroup,
|
||||||
|
Map as LMap,
|
||||||
|
Popup,
|
||||||
|
TileLayer
|
||||||
|
} from 'leaflet';
|
||||||
|
import 'leaflet-easybutton';
|
||||||
|
import { BannedArea, BannedAreas, NWRElement, tagsToHTML } from './overpass.ts';
|
||||||
|
|
||||||
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
const m_mono = new TileLayer(
|
||||||
<div>
|
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
{
|
||||||
<img src="${viteLogo}" class="logo" alt="Vite logo" />
|
attribution:
|
||||||
</a>
|
'Kartendaten © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> Mitwirkende'
|
||||||
<a href="https://www.typescriptlang.org/" target="_blank">
|
}
|
||||||
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
|
);
|
||||||
</a>
|
|
||||||
<h1 class="text-3xl font-bold underline">Vite + TypeScript</h1>
|
|
||||||
<div class="card">
|
|
||||||
<button id="counter" type="button"></button>
|
|
||||||
</div>
|
|
||||||
<p class="read-the-docs">
|
|
||||||
Click on the Vite and TypeScript logos to learn more
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setupCounter(document.querySelector<HTMLButtonElement>('#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(
|
||||||
|
`<p>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äß <a href="https://dserver.bundestag.de/btd/20/087/2008704.pdf" target="_blank">Gesetzentwurf § 5 Konsumverbot</a>) gezeichnet.</p>
|
||||||
|
<p>Oben rechts können die verschiedenen Arten von Orten an- und abgeschaltet werden</p>
|
||||||
|
<p>Datenquelle: <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMaps</a> via <a href="https://overpass-turbo.eu/" target="_blank">Overpass</a></p>
|
||||||
|
<p>Keine Garantie für Korrektheit!</p>`
|
||||||
|
)
|
||||||
|
.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(
|
||||||
|
`<div class="whitespace-pre-wrap">${tagsToHTML({ reason, ...e.tags })}</div>`,
|
||||||
|
{ 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]) => [
|
||||||
|
`<i class="w-3 h-3 my-auto inline-block" style="background-color: ${colorMap[k as BannedArea]}"></i> ${k}`,
|
||||||
|
v
|
||||||
|
])
|
||||||
|
)
|
||||||
|
).addTo(map);
|
||||||
|
|
||||||
|
easyButton(
|
||||||
|
'<span class="bg-white p-2 rounded" title="Standort">📍</span>',
|
||||||
|
function () {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(p) => map.panTo([p.coords.latitude, p.coords.longitude]),
|
||||||
|
undefined,
|
||||||
|
{ enableHighAccuracy: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
).addTo(map);
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export interface APIElement {
|
||||||
|
id: string;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<NodeElement | WayElement | RelationElement>;
|
||||||
|
|
||||||
|
export function tagsToHTML(tags: APIElement['tags']) {
|
||||||
|
const entries = Object.entries(tags);
|
||||||
|
|
||||||
|
let keyValues = entries.map(
|
||||||
|
([k, v]) => `<span class="font-bold">${k}:</span><span class="">${v}</span>`
|
||||||
|
);
|
||||||
|
return `<div class="grid grid-cols-[max-content_1fr] gap-2">${keyValues.join('')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user