Add bicycle parking loading from overpass API

load-bicycle-parking
Jake Coppinger 2023-01-26 18:59:33 +11:00
rodzic 84a8f2b452
commit 0b9ffb2ed1
7 zmienionych plików z 278 dodań i 1 usunięć

22
package-lock.json wygenerowano
Wyświetl plik

@ -12,6 +12,7 @@
"@mapbox/mapbox-gl-directions": "^4.1.1",
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
"@svgr/webpack": "4.3.3",
"@types/debounce": "^1.2.1",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"babel-eslint": "10.1.0",
@ -22,6 +23,7 @@
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "3.4.2",
"debounce": "^1.2.1",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
@ -2396,6 +2398,11 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"node_modules/@types/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA=="
},
"node_modules/@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@ -5741,6 +5748,11 @@
"webidl-conversions": "^4.0.2"
}
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
},
"node_modules/debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -20079,6 +20091,11 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA=="
},
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@ -22833,6 +22850,11 @@
}
}
},
"debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",

Wyświetl plik

@ -7,6 +7,7 @@
"@mapbox/mapbox-gl-directions": "^4.1.1",
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
"@svgr/webpack": "4.3.3",
"@types/debounce": "^1.2.1",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"babel-eslint": "10.1.0",
@ -17,6 +18,7 @@
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "3.4.2",
"debounce": "^1.2.1",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",

99
src/api.ts 100644
Wyświetl plik

@ -0,0 +1,99 @@
import debounce from "debounce";
import { OverpassResponse, RawOverpassNode } from "./interfaces";
import * as http from "https";
import { drawMarkersAndCards, removeMarkers } from "./drawing";
import { wayToNode } from "./geo-utils";
// southern-most latitude, western-most longitude, northern-most latitude, eastern-most longitude.
export async function getOSMData(bounds: number[]): Promise<OverpassResponse> {
const options = {
hostname: "overpass-api.de",
port: 443,
path: "/api/interpreter",
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
console.log("Started POST request...");
const boundsStr = bounds.join(",");
const request_str = `
[out:json][timeout:25];
(
// query part for: “bicycle_parking=*”
node["bicycle_parking"](${boundsStr});
way["bicycle_parking"](${boundsStr});
relation["bicycle_parking"](${boundsStr});
// query part for: “amenity=bicycle_parking”
node["amenity"="bicycle_parking"](${boundsStr});
way["amenity"="bicycle_parking"](${boundsStr});
relation["amenity"="bicycle_parking"](${boundsStr});
);
out body;
>;
out skel qt;
`;
console.log("request:", request_str);
return new Promise((resolve, reject) => {
var req = http.request(options, function (res) {
var body = "";
res.setEncoding("utf8");
res.on("data", (chunk) => (body += chunk));
res.on("end", function () {
if (res.statusCode !== 200) {
console.log("error code", res.statusCode);
reject(res.statusCode);
}
const jsonResponse = JSON.parse(body);
const bars = jsonResponse.elements;
resolve(bars);
});
});
req.on("error", function (e) {
reject(e.message);
});
req.write(request_str);
req.end();
});
}
export const debouncedFetchAndDrawMarkers = debounce(fetchAndDrawMarkers, 2000);
async function fetchAndDrawMarkers(
map: mapboxgl.Map,
markers: React.MutableRefObject<mapboxgl.Marker[]>,
setLoadingStatus: React.Dispatch<React.SetStateAction<LoadingStatusType>>
) {
setLoadingStatus("loading");
const bounds = map.getBounds();
const southernLat = bounds.getSouth();
const westLong = bounds.getWest();
const northLat = bounds.getNorth();
const eastLong = bounds.getEast();
const overpassBounds = [southernLat, westLong, northLat, eastLong];
console.log("getting ads");
let ads: OverpassResponse;
try {
ads = await getOSMData(overpassBounds);
} catch (e) {
console.log("Error:", e);
setLoadingStatus("unknownerror");
return;
}
removeMarkers(markers.current);
const nodesAndWayCenters: RawOverpassNode[] = ads
.map((item) => (item.type === "way" ? wayToNode(item, ads) : item))
.filter((item) => item !== null)
.map((item) => item as RawOverpassNode)
.filter((item) => item.tags !== undefined);
markers.current = await drawMarkersAndCards(map, nodesAndWayCenters);
setLoadingStatus("success");
}
type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror";

68
src/drawing.ts 100644
Wyświetl plik

@ -0,0 +1,68 @@
import mapboxgl from "mapbox-gl";
import {
RawOverpassNode,
} from "./interfaces";
export function drawMarkerAndCard(
item: RawOverpassNode,
map: mapboxgl.Map
): mapboxgl.Marker {
const { lat, lon } = item;
let markerOptions: mapboxgl.MarkerOptions = {};
markerOptions.color = "gray";
const defaultScale = 0.5;
markerOptions.scale = defaultScale;
if (item.tags && item.tags.capacity !== undefined) {
const capacity = parseInt(item.tags.capacity);
console.log({ capacity });
let possibleScale = defaultScale + capacity / 30;
if (possibleScale > 2) {
possibleScale = 2;
}
markerOptions.scale = possibleScale;
if (item.tags && item.tags.covered === "yes") {
markerOptions.color = "green";
}
if (item.tags && item.tags.lit === "yes") {
markerOptions.color = "yellow";
}
if (item.tags && item.tags.bicycle_parking === "shed") {
markerOptions.color = "#00ec18";
}
}
const marker = new mapboxgl.Marker(markerOptions)
.setLngLat([lon, lat])
.addTo(map);
if (window.orientation !== undefined) {
marker.getElement().addEventListener("click", (e) => {
map.flyTo({
center: [lon, lat],
});
});
}
return marker;
}
export function removeMarkers(markers: mapboxgl.Marker[]): void {
markers.map((marker) => marker.remove());
}
export function drawMarkersAndCards(
map: mapboxgl.Map,
items: RawOverpassNode[]
): mapboxgl.Marker[] {
const markers = items
.filter((item) => item.type === "node")
.map((node: RawOverpassNode) => {
return drawMarkerAndCard(node, map);
});
return markers;
}

26
src/geo-utils.ts 100644
Wyświetl plik

@ -0,0 +1,26 @@
import { RawOverpassNode, RawOverpassWay } from "./interfaces.js";
export function wayToNode(way: RawOverpassWay, allItems: (RawOverpassNode | RawOverpassWay)[]): RawOverpassNode | null {
console.log("Converting way...");
console.log(way)
const nodes: number[] = way.nodes;
const firstNodeId = nodes[0];
const firstNode = allItems
.find(item => item.type === 'node' && item.id === firstNodeId) as (RawOverpassNode | undefined)
if (firstNode === undefined) {
console.error(`Unable to find node with id ${firstNodeId}`);
return null;
}
const representativeNode: RawOverpassNode = firstNode;
const { lat, lon } = representativeNode;
const { id, tags } = way;
const output: RawOverpassNode = {
id,
lat,
lon,
type: 'node',
tags
}
console.log({ output });
return output;
}

34
src/interfaces.ts 100644
Wyświetl plik

@ -0,0 +1,34 @@
interface BicycleParkingInterface {
amenity?: "bicycle_parking";
capacity?: string;
covered?: "yes" | "no";
lit?: "yes" | "no";
bicycle_parking?:
| "stands"
| "wall_loops"
| "rack"
| "safe_loops"
| "shed"
| "bollard"
| "lockers"
| "building";
}
export interface RawOverpassNode {
type: "node";
id: number;
lat: number;
lon: number;
tags?: BicycleParkingInterface;
}
export interface RawOverpassWay {
type: "way";
id: number;
nodes: number[],
tags?: BicycleParkingInterface;
}
export type OverpassResponse = (RawOverpassNode | RawOverpassWay)[];
export type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror";

Wyświetl plik

@ -9,6 +9,8 @@ import "@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions.css";
import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
import "@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css";
import { debouncedFetchAndDrawMarkers } from "./api";
import { LoadingStatusType } from "./interfaces";
const MAPBOX_TOKEN =
"pk.eyJ1IjoiamFrZWMiLCJhIjoiY2tkaHplNGhjMDAyMDJybW4ybmRqbTBmMyJ9.AR_fnEuka8-cFb4Snp3upw";
@ -17,6 +19,9 @@ mapboxgl.accessToken = MAPBOX_TOKEN;
export function Map() {
const mapContainer = React.useRef<HTMLDivElement>(null);
const mapRef = React.useRef<mapboxgl.Map | null>(null);
const markers = React.useRef<mapboxgl.Marker[]>([]);
const [loadingStatus, setLoadingStatus] =
useState<LoadingStatusType>("success");
const [lng, setLng] = useState(151.2160755932166);
const [lat, setLat] = useState(-33.88056647217827);
@ -100,12 +105,33 @@ export function Map() {
setLat(map.getCenter().lat);
setZoom(map.getZoom());
});
});
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
map.on("moveend", async () => {
if (map === null) {
return;
}
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
});
});
const statusMessages = {
loading: "Loading from OpenStreetMap...",
success: "Done loading",
unknownerror: "Error loading data. Please wait a bit",
"429error": "Too many requests, please try in a bit",
};
const statusText = statusMessages[loadingStatus];
return (
<div>
<div className="sidebar">
<label>
{" "}
{statusText} |{" "}
A work in progress side project by{" "}
<a
target="_blank"