Rework
This commit is contained in:
+61
-18
@@ -1,37 +1,80 @@
|
|||||||
import * as L from 'leaflet';
|
import {
|
||||||
import { BannedArea, BannedAreas, NWRElement, tagsToHTML } from './overpass.ts';
|
BannedArea,
|
||||||
|
BannedAreas,
|
||||||
|
isNode,
|
||||||
|
isRelation,
|
||||||
|
isWay,
|
||||||
|
NWRElement,
|
||||||
|
tagsToHTML
|
||||||
|
} from './overpass.ts';
|
||||||
|
import { Circle, FeatureGroup, Polygon } from 'leaflet';
|
||||||
|
|
||||||
export class ExclusionCircle extends L.Circle {
|
export class ExclusionCircle extends FeatureGroup<Polygon | Circle> {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly element: NWRElement;
|
readonly element: NWRElement;
|
||||||
readonly reason: BannedArea;
|
readonly reason: BannedArea;
|
||||||
|
|
||||||
constructor(e: NWRElement, options?: L.CircleOptions) {
|
constructor(e: NWRElement) {
|
||||||
const latLng: [number, number] =
|
super();
|
||||||
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);
|
const reason = getReason(e);
|
||||||
super(latLng, {
|
const color = colorMap[getReason(e)];
|
||||||
radius: 100,
|
switch (e.type) {
|
||||||
stroke: false,
|
case 'node':
|
||||||
fillOpacity: 0.3,
|
this.addLayer(this.getCircle(e, color));
|
||||||
fillColor: colorMap[reason],
|
break;
|
||||||
...options
|
case 'way':
|
||||||
|
if (e.geometry.length > 3) {
|
||||||
|
this.addLayer(this.getPolygon(e.geometry, color));
|
||||||
|
} else {
|
||||||
|
this.addLayer(this.getCircle(e.geometry[0], color));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'relation':
|
||||||
|
e.members
|
||||||
|
.filter((m) => !isRelation(m))
|
||||||
|
.forEach((m) => {
|
||||||
|
if (isWay(m)) {
|
||||||
|
m.geometry.length > 3
|
||||||
|
? this.addLayer(this.getPolygon(m.geometry, color))
|
||||||
|
: this.addLayer(this.getCircle(m.geometry[0], color));
|
||||||
|
} else if (isNode(m)) {
|
||||||
|
this.addLayer(this.getCircle(m, color));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.id = e.id;
|
this.id = e.id;
|
||||||
this.element = e;
|
this.element = e;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
|
|
||||||
this.bindPopup(
|
this.bindPopup(
|
||||||
`<div class="whitespace-pre-wrap">${tagsToHTML({ reason, ...e.tags })}</div>`,
|
`<div class="whitespace-pre-wrap flex flex-col gap-3">${tagsToHTML({ reason, ...e.tags })}<a href="https://www.openstreetmap.org/${e.type}/${e.id}" target="_blank">↗ Auf OpenStreetMap öffnen</a></div>`,
|
||||||
{ maxWidth: 500 }
|
{ maxWidth: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPolygon(g: { lat: number; lon: number }[], fillColor: string) {
|
||||||
|
return new Polygon(
|
||||||
|
g.map(({ lat, lon }) => ({ lat, lng: lon })),
|
||||||
|
{
|
||||||
|
fillColor,
|
||||||
|
fillOpacity: 0.4,
|
||||||
|
fill: true,
|
||||||
|
stroke: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCircle(g: { lat: number; lon: number }, fillColor: string) {
|
||||||
|
return new Circle([g.lat, g.lon], {
|
||||||
|
radius: 10,
|
||||||
|
fillColor,
|
||||||
|
fillOpacity: 0.4,
|
||||||
|
fill: true,
|
||||||
|
stroke: false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colorMap: { [reason in BannedArea]: string } = {
|
export const colorMap: { [reason in BannedArea]: string } = {
|
||||||
|
|||||||
+84
-39
@@ -6,15 +6,21 @@ import {
|
|||||||
FeatureGroup,
|
FeatureGroup,
|
||||||
GeoJSON,
|
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 } from './overpass.ts';
|
import { BannedArea, BannedAreas, NWRElement } from './overpass.ts';
|
||||||
import { colorMap, ExclusionCircle, getReason } from './ExclusionCircle.ts';
|
import { colorMap, ExclusionCircle } from './ExclusionCircle.ts';
|
||||||
import { buffer, dissolve, featureCollection } from '@turf/turf';
|
import {
|
||||||
import { Feature } from '@turf/helpers';
|
buffer,
|
||||||
|
dissolve,
|
||||||
|
featureCollection,
|
||||||
|
FeatureCollection,
|
||||||
|
truncate
|
||||||
|
} from '@turf/turf';
|
||||||
|
// @ts-ignore
|
||||||
|
import type { 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',
|
||||||
@@ -31,13 +37,22 @@ const features: { [reason in BannedArea]: FeatureGroup<ExclusionCircle> } = {
|
|||||||
[BannedAreas.PEDESTRIAN]: new FeatureGroup(),
|
[BannedAreas.PEDESTRIAN]: new FeatureGroup(),
|
||||||
[BannedAreas.OTHER]: new FeatureGroup()
|
[BannedAreas.OTHER]: new FeatureGroup()
|
||||||
};
|
};
|
||||||
|
const exclusionZone = new FeatureGroup<GeoJSON>();
|
||||||
|
|
||||||
const center = [53.0552419, 8.7712428] as [number, number];
|
const buffers: { [reason in BannedArea]: (FeatureCollection | Feature)[] } = {
|
||||||
|
[BannedAreas.SCHOOL]: [],
|
||||||
|
[BannedAreas.UNIVERSITY]: [],
|
||||||
|
[BannedAreas.SPORT]: [],
|
||||||
|
[BannedAreas.PEDESTRIAN]: [],
|
||||||
|
[BannedAreas.OTHER]: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const center = [53.0711829, 8.8087718] as [number, number];
|
||||||
const map = new LMap('map', {
|
const map = new LMap('map', {
|
||||||
center,
|
center,
|
||||||
zoom: 19,
|
zoom: 14,
|
||||||
zoomControl: true,
|
zoomControl: true,
|
||||||
layers: [m_mono, ...Object.values(features)]
|
layers: [m_mono, exclusionZone, ...Object.values(features)]
|
||||||
});
|
});
|
||||||
|
|
||||||
new Popup()
|
new Popup()
|
||||||
@@ -54,13 +69,51 @@ map.on('moveend', async () => {
|
|||||||
await fetchData();
|
await fetchData();
|
||||||
});
|
});
|
||||||
let loading = false;
|
let loading = false;
|
||||||
const areas = new FeatureGroup();
|
|
||||||
let exclusionZone: GeoJSON | undefined;
|
function showBuffer() {
|
||||||
|
console.time('prepare');
|
||||||
|
map.attributionControl.setPrefix('Berechne Zone');
|
||||||
|
const fs = Object.entries(buffers)
|
||||||
|
.filter(([k]) => map.hasLayer(features[k as keyof typeof features]))
|
||||||
|
.flatMap(([_, buff]) => {
|
||||||
|
const fs: Feature[] = buff.flatMap((f) =>
|
||||||
|
f.type === 'Feature' ? [f] : f.features
|
||||||
|
);
|
||||||
|
return fs;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fs.length > 0) {
|
||||||
|
exclusionZone.clearLayers();
|
||||||
|
console.timeEnd('prepare');
|
||||||
|
try {
|
||||||
|
console.time('dissolve');
|
||||||
|
const b = dissolve(featureCollection(fs));
|
||||||
|
const zone = new GeoJSON(b, {
|
||||||
|
style: { color: 'red', fillOpacity: 0.1, weight: 1 }
|
||||||
|
});
|
||||||
|
console.timeEnd('dissolve');
|
||||||
|
console.time('render');
|
||||||
|
exclusionZone.addLayer(zone);
|
||||||
|
console.timeEnd('render');
|
||||||
|
exclusionZone.bringToBack();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('dissolve', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.attributionControl.setPrefix('');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(features).forEach((f) => {
|
||||||
|
f.on('add', showBuffer);
|
||||||
|
f.on('remove', showBuffer);
|
||||||
|
});
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
if (map.getZoom() < 12 || loading) {
|
if (map.getZoom() < 12 || loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading = true;
|
loading = true;
|
||||||
|
map.attributionControl.setPrefix('Lädt...');
|
||||||
const bounds = map.getBounds();
|
const bounds = map.getBounds();
|
||||||
const query = `[out:json][timeout:25][bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}];
|
const query = `[out:json][timeout:25][bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}];
|
||||||
(
|
(
|
||||||
@@ -91,55 +144,47 @@ out geom;`;
|
|||||||
.catch(() => ({ elements: [] }))
|
.catch(() => ({ elements: [] }))
|
||||||
.then((r) => r.elements as NWRElement[]);
|
.then((r) => r.elements as NWRElement[]);
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
console.time('parse');
|
||||||
|
map.attributionControl.setPrefix('Berechne Orte...');
|
||||||
const ids = elements.map((e) => e.id);
|
const ids = elements.map((e) => e.id);
|
||||||
const exclude: string[] = [];
|
const exclude: ExclusionCircle[] = [];
|
||||||
|
Object.values(buffers).forEach((b) => b.splice(0));
|
||||||
Object.values(features).forEach((f) =>
|
Object.values(features).forEach((f) =>
|
||||||
f.eachLayer((l) => {
|
f.eachLayer((l) => {
|
||||||
const id = (l as ExclusionCircle).id;
|
const id = (l as ExclusionCircle).id;
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
exclude.push(id);
|
exclude.push(l as ExclusionCircle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
f.removeLayer(l);
|
f.removeLayer(l);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const buffers: Feature[] = [];
|
|
||||||
elements.forEach((e) => {
|
elements.forEach((e) => {
|
||||||
if (exclude.includes(e.id)) {
|
let marker = exclude.find((it) => it.id === e.id);
|
||||||
return;
|
if (!marker) {
|
||||||
|
marker = new ExclusionCircle(e);
|
||||||
}
|
}
|
||||||
const marker = new ExclusionCircle(e);
|
|
||||||
features[marker.reason].addLayer(marker);
|
|
||||||
|
|
||||||
if (e.type === 'way') {
|
features[marker.reason].addLayer(marker);
|
||||||
const area = new Polygon(
|
try {
|
||||||
e.geometry.map(({ lat, lon }) => ({ lat, lng: lon })),
|
const b = buffer(marker.toGeoJSON(), 100, { units: 'meters' });
|
||||||
{
|
|
||||||
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);
|
// new GeoJSON(b).addTo(map);
|
||||||
buffers.push(b);
|
buffers[marker.reason].push(truncate(b));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Buffer calc for id=${e.id}, type=${e.type} failed`,
|
||||||
|
e,
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.timeEnd('parse');
|
||||||
|
|
||||||
// const forbidden = buffers.reduce((u, b) => union(u, b), buffers.pop());
|
queueMicrotask(showBuffer);
|
||||||
// new GeoJSON(forbidden).addTo(map);
|
map.attributionControl.setPrefix('');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new Control.Layers(
|
new Control.Layers(
|
||||||
|
|||||||
+16
-6
@@ -29,16 +29,26 @@ export interface RelationElement extends APIElement {
|
|||||||
maxlat: number;
|
maxlat: number;
|
||||||
maxlon: number;
|
maxlon: number;
|
||||||
};
|
};
|
||||||
members: {
|
members: (Omit<
|
||||||
type: 'way';
|
NodeElement | WayElement | Omit<RelationElement, 'members'>,
|
||||||
ref: number;
|
'id' | 'tags' | 'bounds'
|
||||||
role: string;
|
> & { role: string })[];
|
||||||
geometry: { lat: number; lon: number }[];
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NWRElement = Readonly<NodeElement | WayElement | RelationElement>;
|
export type NWRElement = Readonly<NodeElement | WayElement | RelationElement>;
|
||||||
|
|
||||||
|
export function isWay(e: { type: NWRElement['type'] }): e is WayElement {
|
||||||
|
return e.type === 'way';
|
||||||
|
}
|
||||||
|
export function isNode(e: { type: NWRElement['type'] }): e is NodeElement {
|
||||||
|
return e.type === 'node';
|
||||||
|
}
|
||||||
|
export function isRelation(e: {
|
||||||
|
type: NWRElement['type'];
|
||||||
|
}): e is RelationElement {
|
||||||
|
return e.type === 'relation';
|
||||||
|
}
|
||||||
|
|
||||||
export function tagsToHTML(tags: APIElement['tags']) {
|
export function tagsToHTML(tags: APIElement['tags']) {
|
||||||
const entries = Object.entries(tags);
|
const entries = Object.entries(tags);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user