Exclusion

This commit is contained in:
2024-03-16 20:04:57 +01:00
parent cb859216f1
commit 9070c26bff
5 changed files with 2161 additions and 63 deletions
+2032 -3
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -22,6 +22,7 @@
"vite": "^4.4.5" "vite": "^4.4.5"
}, },
"dependencies": { "dependencies": {
"@turf/turf": "^6.5.0",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-easybutton": "^2.4.0" "leaflet-easybutton": "^2.4.0"
+72
View File
@@ -0,0 +1,72 @@
import * as L from 'leaflet';
import { BannedArea, BannedAreas, NWRElement, tagsToHTML } from './overpass.ts';
export class ExclusionCircle extends L.Circle {
readonly id: string;
readonly element: NWRElement;
readonly reason: BannedArea;
constructor(e: NWRElement, options?: L.CircleOptions) {
const latLng: [number, number] =
e.type === 'node'
? [e.lat, e.lon]
: [
(e.bounds.maxlat + e.bounds.minlat) / 2,
(e.bounds.maxlon + e.bounds.minlon) / 2
];
const reason = getReason(e);
super(latLng, {
radius: 100,
stroke: false,
fillOpacity: 0.3,
fillColor: colorMap[reason],
...options
});
this.id = e.id;
this.element = e;
this.reason = reason;
this.bindPopup(
`<div class="whitespace-pre-wrap">${tagsToHTML({ reason, ...e.tags })}</div>`,
{ maxWidth: 500 }
);
}
}
export const colorMap: { [reason in BannedArea]: string } = {
[BannedAreas.SCHOOL]: 'red',
[BannedAreas.UNIVERSITY]: 'orange',
[BannedAreas.SPORT]: 'yellow',
[BannedAreas.PEDESTRIAN]: 'green',
[BannedAreas.OTHER]: 'blue'
};
export 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;
}
View File
+56 -60
View File
@@ -1,16 +1,20 @@
import './style.css'; import './style.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { import {
Circle,
Control, Control,
easyButton, easyButton,
FeatureGroup, FeatureGroup,
GeoJSON,
Map as LMap, Map as LMap,
Polygon,
Popup, Popup,
TileLayer TileLayer
} from 'leaflet'; } from 'leaflet';
import 'leaflet-easybutton'; import 'leaflet-easybutton';
import { BannedArea, BannedAreas, NWRElement, tagsToHTML } from './overpass.ts'; import { BannedArea, BannedAreas, NWRElement } from './overpass.ts';
import { colorMap, ExclusionCircle, getReason } from './ExclusionCircle.ts';
import { buffer, dissolve, featureCollection } from '@turf/turf';
import { Feature } from '@turf/helpers';
const m_mono = new TileLayer( const m_mono = new TileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
@@ -20,7 +24,7 @@ const m_mono = new TileLayer(
} }
); );
const features: { [reason in BannedArea]: FeatureGroup } = { const features: { [reason in BannedArea]: FeatureGroup<ExclusionCircle> } = {
[BannedAreas.SCHOOL]: new FeatureGroup(), [BannedAreas.SCHOOL]: new FeatureGroup(),
[BannedAreas.UNIVERSITY]: new FeatureGroup(), [BannedAreas.UNIVERSITY]: new FeatureGroup(),
[BannedAreas.SPORT]: new FeatureGroup(), [BannedAreas.SPORT]: new FeatureGroup(),
@@ -28,10 +32,10 @@ const features: { [reason in BannedArea]: FeatureGroup } = {
[BannedAreas.OTHER]: new FeatureGroup() [BannedAreas.OTHER]: new FeatureGroup()
}; };
const center = [53.0711829, 8.8087718] as [number, number]; const center = [53.0552419, 8.7712428] as [number, number];
const map = new LMap('map', { const map = new LMap('map', {
center, center,
zoom: 14, zoom: 19,
zoomControl: true, zoomControl: true,
layers: [m_mono, ...Object.values(features)] layers: [m_mono, ...Object.values(features)]
}); });
@@ -39,7 +43,7 @@ const map = new LMap('map', {
new Popup() new Popup()
.setLatLng(center) .setLatLng(center)
.setContent( .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>Diese Karte zeigt die Orte, an denen der Konsum von Cannabis verboten ist. Um jeden Ort wird ein Kreis mit Radius von 100 Metern (gemäß <a href="https://dserver.bundestag.de/btd/20/104/2010426.pdf" target="_blank">Beschlussempfehlung § 5 Konsumverbot</a>) gezeichnet.</p>
<p>Oben rechts können die verschiedenen Arten von Orten an- und abgeschaltet werden</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>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>` <p>Keine Garantie für Korrektheit!</p>`
@@ -50,6 +54,8 @@ map.on('moveend', async () => {
await fetchData(); await fetchData();
}); });
let loading = false; let loading = false;
const areas = new FeatureGroup();
let exclusionZone: GeoJSON | undefined;
async function fetchData() { async function fetchData() {
if (map.getZoom() < 12 || loading) { if (map.getZoom() < 12 || loading) {
return; return;
@@ -85,67 +91,57 @@ out geom;`;
.catch(() => ({ elements: [] })) .catch(() => ({ elements: [] }))
.then((r) => r.elements as NWRElement[]); .then((r) => r.elements as NWRElement[]);
Object.values(features).forEach((f) => f.clearLayers()); const ids = elements.map((e) => e.id);
const exclude: string[] = [];
elements.forEach((e) => { Object.values(features).forEach((f) =>
const reason = getReason(e); f.eachLayer((l) => {
const marker = new Circle( const id = (l as ExclusionCircle).id;
e.type === 'node' ? [e.lat, e.lon] : [e.bounds.maxlat, e.bounds.maxlon], if (ids.includes(id)) {
200, exclude.push(id);
{ return;
radius: 200,
stroke: false,
fillOpacity: 0.3,
fillColor: colorMap[reason]
} }
).bindPopup( f.removeLayer(l);
`<div class="whitespace-pre-wrap">${tagsToHTML({ reason, ...e.tags })}</div>`, })
{ maxWidth: 500 } );
);
features[reason].addLayer(marker); const buffers: Feature[] = [];
elements.forEach((e) => {
if (exclude.includes(e.id)) {
return;
}
const marker = new ExclusionCircle(e);
features[marker.reason].addLayer(marker);
if (e.type === 'way') {
const area = new Polygon(
e.geometry.map(({ lat, lon }) => ({ lat, lng: lon })),
{
fillColor: colorMap[getReason(e)],
fillOpacity: 0.4,
fill: true,
stroke: false
}
);
area.addTo(map);
const b = buffer(area.toGeoJSON(), 100, { units: 'meters' });
// new GeoJSON(b).addTo(map);
buffers.push(b);
}
}); });
// const forbidden = buffers.reduce((u, b) => union(u, b), buffers.pop());
// new GeoJSON(forbidden).addTo(map);
const b = dissolve(featureCollection(buffers));
exclusionZone?.removeFrom(map);
exclusionZone = new GeoJSON(b, {
style: { color: 'red', fillOpacity: 0.1, weight: 1 }
});
exclusionZone.addTo(map);
loading = false; 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( new Control.Layers(
{}, {},
Object.fromEntries( Object.fromEntries(