kopia lustrzana https://github.com/jakecoppinger/safe-cycling-map
Porównaj commity
12 Commity
43166ef98a
...
0234c6a747
Autor | SHA1 | Data |
---|---|---|
Jake Coppinger | 0234c6a747 | |
Jake Coppinger | 0c13b5b909 | |
Jake Coppinger | 68cd11a980 | |
Jake Coppinger | e484e23299 | |
Jake Coppinger | a7110eef7f | |
Jake Coppinger | 7fbd8ac1d3 | |
Jake Coppinger | 66a3ad80f3 | |
Jake Coppinger | 8f4446b462 | |
Jake Coppinger | 865be63b88 | |
Jake Coppinger | 9861006cf1 | |
Jake Coppinger | 26feb770aa | |
Jake Coppinger | 751eae09ab |
21
README.md
21
README.md
|
@ -1,8 +1,26 @@
|
||||||
Safe Cycling Map
|
Safe Cycling Map
|
||||||
================
|
================
|
||||||
|
|
||||||
Work in progress! PRs and forks very welcome :)
|
A map showing how safe a street is for cycling, based on (arbitrary) metrics. See the
|
||||||
|
[key](https://github.com/jakecoppinger/safe-cycling-map/blob/main/key.md) for how street safety is calculated.
|
||||||
|
|
||||||
|
This is a work in progress side project. This data is not guaranteed to be accurate.
|
||||||
|
|
||||||
|
When zoomed in close, individual road and bicycle lanes are shown. When zoomed out, streets are
|
||||||
|
coloured by their safety ratings.
|
||||||
|
|
||||||
|
# Disclaimer
|
||||||
|
Warning: This is an arbitrary rating system. Data is not guaranteed to be accurate.
|
||||||
|
|
||||||
|
This map uses OpenStreetMap data. It is not a complete or accurate map of the world and should not
|
||||||
|
be used in such a manner that deficiencies, omissions, inaccuracies or errors could result in death,
|
||||||
|
loss or injury. The maps are an iterative ongoing work-in-progress and everyone is welcome to
|
||||||
|
contribute editing the OpenStreetMap data if you spot inaccuracies. (warning courtesy of [CyclOSM](https://www.cyclosm.org/))
|
||||||
|
|
||||||
|
# Contributing: Found a mislabelled street? You can fix it!
|
||||||
|
|
||||||
|
Head to https://bikemaps.org/blog/post/improving-bicycling-data-on-openstreetmap for instructions
|
||||||
|
on how to fix OpenStreetMap data.
|
||||||
|
|
||||||
![Screenshot of map](img/safe-cycling-map-2022-01-05-v2.jpg)
|
![Screenshot of map](img/safe-cycling-map-2022-01-05-v2.jpg)
|
||||||
|
|
||||||
|
@ -12,6 +30,7 @@ Uses [osm2streets-vector-tileserver](https://github.com/jakecoppinger/osm2street
|
||||||
a vector tileserver I wrote to generate Protobuf GeoJSON vector tiles using the JS bindings to
|
a vector tileserver I wrote to generate Protobuf GeoJSON vector tiles using the JS bindings to
|
||||||
osm2streets (which is written in Rust).
|
osm2streets (which is written in Rust).
|
||||||
|
|
||||||
|
|
||||||
# Local development
|
# Local development
|
||||||
|
|
||||||
See instructions for setting up the backend tileserve at
|
See instructions for setting up the backend tileserve at
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
Safe Cycling Map
|
||||||
|
================
|
||||||
|
Zoom in to see individual lanes, zoom out a little to see street safety.
|
||||||
|
|
||||||
|
Warning: This is an arbitrary rating system. Data is not guaranteed to be accurate.
|
||||||
|
|
||||||
|
This map uses OpenStreetMap data. It is not a complete or accurate map of the world and should not
|
||||||
|
be used in such a manner that deficiencies, omissions, inaccuracies or errors could result in death,
|
||||||
|
loss or injury. The maps are an iterative ongoing work-in-progress and everyone is welcome to
|
||||||
|
contribute editing the OpenStreetMap data if you spot inaccuracies. (warning courtesy of [CyclOSM](https://www.cyclosm.org/))
|
||||||
|
|
||||||
|
See the README at https://github.com/jakecoppinger/safe-cycling-map for how to fix the data (or
|
||||||
|
propose improvments to the safety rating system).
|
||||||
|
|
||||||
|
# Key
|
||||||
|
## Safe streets - green
|
||||||
|
- Speed is less than or equal to 30kph
|
||||||
|
- Is a [living street](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dliving_street)
|
||||||
|
- Is a separated cycleway
|
||||||
|
- Is a cycle lane separated from the road
|
||||||
|
- Is a shared path (bikes + pedestrians allowed)
|
||||||
|
|
||||||
|
## More dangerous streets
|
||||||
|
- Road has a speed limit less than 40kph and greater than 30kmh
|
||||||
|
- Has an on road, painted (non-separated) bike lane
|
||||||
|
|
||||||
|
## Dangerous streets
|
||||||
|
- Speed is higher than 40kmh
|
||||||
|
- Road is a residental street with default speed limit (50kph)
|
||||||
|
|
||||||
|
## TODO: Banned streets
|
||||||
|
Currently banned streets (eg. motorways/Sydney Harbour Bridge) are currently displayed as red.
|
|
@ -104,7 +104,7 @@ h1 {
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
}
|
}
|
||||||
.mapboxgl-ctrl-top-left {
|
.mapboxgl-ctrl-top-left {
|
||||||
margin-top: 50px;
|
margin-top: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-geocoder--icon.mapboxgl-ctrl-geocoder--icon-search {
|
.mapboxgl-ctrl-geocoder--icon.mapboxgl-ctrl-geocoder--icon-search {
|
||||||
|
|
81
src/api.ts
81
src/api.ts
|
@ -1,10 +1,9 @@
|
||||||
import debounce from "debounce";
|
import debounce from "debounce";
|
||||||
import { OverpassResponse, RawOverpassNode } from "./interfaces";
|
import { LoadingStatusType, OverpassResponse} from "./interfaces";
|
||||||
|
|
||||||
import * as http from "https";
|
import * as http from "https";
|
||||||
import { drawMarkersAndCards, removeMarkers } from "./drawing";
|
import { addStreetLayers, removeStreetLayers } from "./drawing";
|
||||||
import { wayToNode } from "./geo-utils";
|
import { safeCycleways } from "./overpass-requests";
|
||||||
import { bicycleParking, safeCycleways } from "./overpass-requests";
|
|
||||||
|
|
||||||
import osmtogeojson from 'osmtogeojson';
|
import osmtogeojson from 'osmtogeojson';
|
||||||
|
|
||||||
|
@ -14,8 +13,10 @@ import osmtogeojson from 'osmtogeojson';
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function getOSMData(overpassQuery: string): Promise<OverpassResponse> {
|
export async function getOSMData(overpassQuery: string): Promise<OverpassResponse> {
|
||||||
|
// overpass.kumi.systems
|
||||||
|
// hostname: "overpass-api.de",
|
||||||
const options = {
|
const options = {
|
||||||
hostname: "overpass-api.de",
|
hostname: "overpass.kumi.systems",
|
||||||
port: 443,
|
port: 443,
|
||||||
path: "/api/interpreter",
|
path: "/api/interpreter",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -64,17 +65,14 @@ async function fetchAndDrawMarkers(
|
||||||
const northLat = bounds.getNorth();
|
const northLat = bounds.getNorth();
|
||||||
const eastLong = bounds.getEast();
|
const eastLong = bounds.getEast();
|
||||||
|
|
||||||
let ads: OverpassResponse;
|
|
||||||
let safeRoutes: OverpassResponse;
|
let safeRoutes: OverpassResponse;
|
||||||
|
|
||||||
const overpassBounds = [southernLat, westLong, northLat, eastLong];
|
const overpassBounds = [southernLat, westLong, northLat, eastLong];
|
||||||
const boundsStr = overpassBounds.join(",");
|
const boundsStr = overpassBounds.join(",");
|
||||||
const parkingOverpassQuery = bicycleParking(boundsStr);;
|
|
||||||
const safeRoutesOverpassQuery = safeCycleways(boundsStr);;
|
const safeRoutesOverpassQuery = safeCycleways(boundsStr);;
|
||||||
|
|
||||||
console.log("Started POST request...");
|
console.log("Started POST request...");
|
||||||
try {
|
try {
|
||||||
ads = (await getOSMData(parkingOverpassQuery));
|
|
||||||
safeRoutes = await getOSMData(safeRoutesOverpassQuery);
|
safeRoutes = await getOSMData(safeRoutesOverpassQuery);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error:", e);
|
console.log("Error:", e);
|
||||||
|
@ -86,71 +84,8 @@ async function fetchAndDrawMarkers(
|
||||||
console.log(geoJson);
|
console.log(geoJson);
|
||||||
console.log("Adding geojson to map...");
|
console.log("Adding geojson to map...");
|
||||||
|
|
||||||
// try {
|
removeStreetLayers(map);
|
||||||
// map.removeSource('greenRoads');
|
addStreetLayers(map, geoJson);
|
||||||
// } catch (e) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
map.addSource('redRoads', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: {
|
|
||||||
features: geoJson.features.filter(feature => feature.properties &&
|
|
||||||
feature.properties.maxspeed > 40),
|
|
||||||
type: "FeatureCollection"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
map.addSource('greenRoads', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: {
|
|
||||||
features: geoJson.features.filter(feature => feature.properties &&
|
|
||||||
(feature.properties.highway === 'cycleway' || feature.properties.highway === 'pedestrian'))
|
|
||||||
,
|
|
||||||
type: "FeatureCollection"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add a new layer to visualize the polygon.
|
|
||||||
map.addLayer({
|
|
||||||
'id': 'redRoadsId',
|
|
||||||
'type': 'line',
|
|
||||||
'source': 'redRoads', // reference the data source
|
|
||||||
'layout': {},
|
|
||||||
'paint': {
|
|
||||||
"line-color": "red",
|
|
||||||
"line-width": 5
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Add a new layer to visualize the polygon.
|
|
||||||
map.addLayer({
|
|
||||||
'id': 'greenRoadsId',
|
|
||||||
'type': 'line',
|
|
||||||
'source': 'greenRoads', // reference the data source
|
|
||||||
'layout': {},
|
|
||||||
'paint': {
|
|
||||||
"line-color": "green",
|
|
||||||
"line-width": 5
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
removeMarkers(markers.current);
|
|
||||||
|
|
||||||
const nodesAndWayCenters: RawOverpassNode[] = ads.elements
|
|
||||||
.map((item) => (item.type === "way" ? wayToNode(item, ads.elements) : item))
|
|
||||||
.filter((item) => item !== null)
|
|
||||||
.map((item) => item as RawOverpassNode)
|
|
||||||
.filter((item) => item.tags !== undefined);
|
|
||||||
|
|
||||||
markers.current = await drawMarkersAndCards(map, nodesAndWayCenters);
|
|
||||||
setLoadingStatus("success");
|
setLoadingStatus("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror";
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
RawOverpassNode,
|
RawOverpassNode,
|
||||||
} from "./interfaces";
|
} from "./interfaces";
|
||||||
|
|
||||||
|
import { FeatureCollection, Geometry, GeoJsonProperties, Feature, GeometryObject } from 'geojson';
|
||||||
|
import { isGreenRoad, isOrangeRoad, isRedRoad } from "./osm-selectors";
|
||||||
|
|
||||||
export function drawMarkerAndCard(
|
export function drawMarkerAndCard(
|
||||||
item: RawOverpassNode,
|
item: RawOverpassNode,
|
||||||
|
@ -53,6 +55,91 @@ export function drawMarkerAndCard(
|
||||||
export function removeMarkers(markers: mapboxgl.Marker[]): void {
|
export function removeMarkers(markers: mapboxgl.Marker[]): void {
|
||||||
markers.map((marker) => marker.remove());
|
markers.map((marker) => marker.remove());
|
||||||
}
|
}
|
||||||
|
export function removeStreetLayers(map: mapboxgl.Map): void {
|
||||||
|
try {
|
||||||
|
console.log("Removing sources...");
|
||||||
|
map.removeLayer('greenRoadsId');
|
||||||
|
map.removeLayer('redRoadsId');
|
||||||
|
map.removeLayer('orangeRoadsId');
|
||||||
|
|
||||||
|
map.removeSource('greenRoads');
|
||||||
|
map.removeSource('redRoads');
|
||||||
|
map.removeSource('orangeRoads');
|
||||||
|
} catch (e) {
|
||||||
|
console.log("not removing sources - at least one doesn't exist yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addStreetLayers(map: mapboxgl.Map, geoJson: FeatureCollection<Geometry, GeoJsonProperties>) {
|
||||||
|
/** Add below first vector layer */
|
||||||
|
const layerToAddBefore = 'SharedUse';
|
||||||
|
map.addSource('redRoads', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
features: geoJson.features.filter(isRedRoad),
|
||||||
|
type: "FeatureCollection"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addSource('orangeRoads', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
features: geoJson.features.filter(isOrangeRoad)
|
||||||
|
,
|
||||||
|
type: "FeatureCollection"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addSource('greenRoads', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
features: geoJson.features.filter(isGreenRoad)
|
||||||
|
,
|
||||||
|
type: "FeatureCollection"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Add a new layer to visualize the polygon.
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'redRoadsId',
|
||||||
|
'type': 'line',
|
||||||
|
'source': 'redRoads', // reference the data source
|
||||||
|
'layout': {},
|
||||||
|
'paint': {
|
||||||
|
"line-color": "red",
|
||||||
|
"line-width": 3,
|
||||||
|
'line-opacity': 0.3
|
||||||
|
},
|
||||||
|
}, layerToAddBefore);
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'orangeRoadsId',
|
||||||
|
'type': 'line',
|
||||||
|
'source': 'orangeRoads', // reference the data source
|
||||||
|
'layout': {},
|
||||||
|
'paint': {
|
||||||
|
"line-color": "orange",
|
||||||
|
"line-width": 3,
|
||||||
|
'line-opacity': 0.5
|
||||||
|
},
|
||||||
|
}, layerToAddBefore);
|
||||||
|
|
||||||
|
|
||||||
|
// Add a new layer to visualize the polygon.
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'greenRoadsId',
|
||||||
|
'type': 'line',
|
||||||
|
'source': 'greenRoads', // reference the data source
|
||||||
|
'layout': {},
|
||||||
|
'paint': {
|
||||||
|
"line-color": "#00FF00",
|
||||||
|
"line-width": 7,
|
||||||
|
'line-opacity': 0.8
|
||||||
|
},
|
||||||
|
}, layerToAddBefore);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export function drawMarkersAndCards(
|
export function drawMarkersAndCards(
|
||||||
map: mapboxgl.Map,
|
map: mapboxgl.Map,
|
||||||
|
|
|
@ -33,4 +33,4 @@ export type OverpassResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror";
|
export type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror" | "too_zoomed_out" | "ready_to_load"
|
|
@ -1,5 +1,6 @@
|
||||||
// TODO: Find the layer of the road labels for the maptiler background
|
// TODO: Find the layer of the road labels for the maptiler background
|
||||||
const layerToAddAfter = undefined;
|
const layerToAddAfter = undefined;
|
||||||
|
// const layerToAddAfter = 'greenRoadsId'; //undefined;
|
||||||
|
|
||||||
function addLayer(
|
function addLayer(
|
||||||
map: mapboxgl.Map,
|
map: mapboxgl.Map,
|
||||||
|
|
83
src/map.tsx
83
src/map.tsx
|
@ -15,13 +15,17 @@ import { LoadingStatusType } from "./interfaces";
|
||||||
const MAPBOX_TOKEN =
|
const MAPBOX_TOKEN =
|
||||||
"pk.eyJ1IjoiamFrZWMiLCJhIjoiY2tkaHplNGhjMDAyMDJybW4ybmRqbTBmMyJ9.AR_fnEuka8-cFb4Snp3upw";
|
"pk.eyJ1IjoiamFrZWMiLCJhIjoiY2tkaHplNGhjMDAyMDJybW4ybmRqbTBmMyJ9.AR_fnEuka8-cFb4Snp3upw";
|
||||||
|
|
||||||
|
const min_overpass_turbo_zoom = 15;
|
||||||
|
/** Also the min zoom of the vector tileserver */
|
||||||
|
// const max_overpass_turbo_zoom = 15;
|
||||||
|
|
||||||
mapboxgl.accessToken = MAPBOX_TOKEN;
|
mapboxgl.accessToken = MAPBOX_TOKEN;
|
||||||
export function Map() {
|
export function Map() {
|
||||||
const mapContainer = React.useRef<HTMLDivElement>(null);
|
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||||
const mapRef = React.useRef<mapboxgl.Map | null>(null);
|
const mapRef = React.useRef<mapboxgl.Map | null>(null);
|
||||||
const markers = React.useRef<mapboxgl.Marker[]>([]);
|
const markers = React.useRef<mapboxgl.Marker[]>([]);
|
||||||
const [loadingStatus, setLoadingStatus] =
|
const [loadingStatus, setLoadingStatus] =
|
||||||
useState<LoadingStatusType>("success");
|
useState<LoadingStatusType>("ready_to_load");
|
||||||
|
|
||||||
const [lng, setLng] = useState(151.2160755932166);
|
const [lng, setLng] = useState(151.2160755932166);
|
||||||
const [lat, setLat] = useState(-33.88056647217827);
|
const [lat, setLat] = useState(-33.88056647217827);
|
||||||
|
@ -41,29 +45,7 @@ export function Map() {
|
||||||
center: [lng, lat],
|
center: [lng, lat],
|
||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
hash: true,
|
hash: true,
|
||||||
style: {
|
style: "mapbox://styles/mapbox/dark-v11",
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
"raster-tiles": {
|
|
||||||
type: "raster",
|
|
||||||
tiles: [
|
|
||||||
"https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution:
|
|
||||||
'© <a target="_blank" href="https://www.cyclosm.org">CyclOSM</a>, <a target="_blank" href="https://github.com/a-b-street/osm2streets">osm2streets</a>, <a target="_blank" href="https://openstreetmap.org/">OpenStreetMap contributors</a>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "simple-tiles",
|
|
||||||
type: "raster",
|
|
||||||
source: "raster-tiles",
|
|
||||||
minzoom: 0,
|
|
||||||
maxzoom: 19,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
@ -78,12 +60,12 @@ export function Map() {
|
||||||
}),
|
}),
|
||||||
"top-left"
|
"top-left"
|
||||||
);
|
);
|
||||||
map.addControl(
|
// map.addControl(
|
||||||
new MapboxDirections({
|
// new MapboxDirections({
|
||||||
accessToken: mapboxgl.accessToken,
|
// accessToken: mapboxgl.accessToken,
|
||||||
}),
|
// }),
|
||||||
"top-left"
|
// "top-left"
|
||||||
);
|
// );
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new mapboxgl.GeolocateControl({
|
new mapboxgl.GeolocateControl({
|
||||||
positionOptions: {
|
positionOptions: {
|
||||||
|
@ -99,55 +81,70 @@ export function Map() {
|
||||||
}
|
}
|
||||||
const { lng, lat } = map.getCenter();
|
const { lng, lat } = map.getCenter();
|
||||||
const zoom = map.getZoom();
|
const zoom = map.getZoom();
|
||||||
|
if (zoom < min_overpass_turbo_zoom) {
|
||||||
|
setLoadingStatus("too_zoomed_out");
|
||||||
|
} else {
|
||||||
|
setLoadingStatus("ready_to_load");
|
||||||
|
}
|
||||||
console.log(lng, lat, zoom);
|
console.log(lng, lat, zoom);
|
||||||
|
|
||||||
setLng(map.getCenter().lng);
|
setLng(map.getCenter().lng);
|
||||||
setLat(map.getCenter().lat);
|
setLat(map.getCenter().lat);
|
||||||
setZoom(map.getZoom());
|
setZoom(map.getZoom());
|
||||||
});
|
});
|
||||||
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
|
|
||||||
|
|
||||||
|
if (map.getZoom() < min_overpass_turbo_zoom) {
|
||||||
|
setLoadingStatus("too_zoomed_out");
|
||||||
|
} else {
|
||||||
|
console.log(`zoom is ${map.getZoom()}`);
|
||||||
|
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
|
||||||
|
}
|
||||||
|
|
||||||
map.on("moveend", async () => {
|
map.on("moveend", async () => {
|
||||||
if (map === null) {
|
if (map === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
if (zoom > min_overpass_turbo_zoom) {
|
||||||
|
console.log(`zoom is ${zoom}`);
|
||||||
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
|
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
const statusMessages = {
|
const statusMessages = {
|
||||||
loading: "Loading from OpenStreetMap...",
|
loading: "Loading from OpenStreetMap...",
|
||||||
success: "Done loading",
|
success: "Done loading",
|
||||||
unknownerror: "Error loading data. Please wait a bit",
|
ready_to_load: "About to load...",
|
||||||
|
too_zoomed_out: "Zoom in to see street safety",
|
||||||
|
unknownerror: "Error loading. Please wait a bit",
|
||||||
"429error": "Too many requests, please try in a bit",
|
"429error": "Too many requests, please try in a bit",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const statusText = statusMessages[loadingStatus];
|
const statusText = statusMessages[loadingStatus];
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<label>
|
<label>
|
||||||
{" "}
|
<span color="red">Warning:</span> Data is open source and not guaranteed to be
|
||||||
{statusText} |{" "}
|
accurate.
|
||||||
A work in progress side project by{" "}
|
<br></br>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://jakecoppinger.com/"
|
href="https://github.com/jakecoppinger/safe-cycling-map/blob/main/key.md"
|
||||||
>
|
>
|
||||||
Jake Coppinger
|
View map key and how safety is calculated
|
||||||
</a>{" "}
|
</a>
|
||||||
| Open source on{" "}
|
<br></br>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://github.com/jakecoppinger/safe-cycling-map"
|
href="https://github.com/jakecoppinger/safe-cycling-map"
|
||||||
>
|
>
|
||||||
Github
|
About this map
|
||||||
</a>
|
</a>
|
||||||
|
<br></br>
|
||||||
|
{statusText}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ref={mapContainer} className="map-container" />
|
<div ref={mapContainer} className="map-container" />
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Geometry, GeoJsonProperties, Feature } from 'geojson';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is a red (dangerous) road if:
|
||||||
|
* - Speed is higher than 40kmh
|
||||||
|
* - Road is a residental street with default speed limit (50kph)
|
||||||
|
* @param feature
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isRedRoad(feature: Feature<Geometry, GeoJsonProperties>): boolean {
|
||||||
|
const p = feature.properties;
|
||||||
|
if (p === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(p.highway === 'primary' && p.maxspeed === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.maxspeed > 40) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.highway === 'residential' && p.maxspeed === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is an orange (caution) road if:
|
||||||
|
* - Road has a speed limit less than 40kph and greater than 30kmh
|
||||||
|
* - Has an on road, painted (non-separated) bike lane
|
||||||
|
* @param feature
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isOrangeRoad(feature: Feature<Geometry, GeoJsonProperties>): boolean {
|
||||||
|
const p = feature.properties;
|
||||||
|
if (p === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.maxspeed <= 40) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.cycleway === 'lane') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is a green (safe) road if:
|
||||||
|
* - Speed is less than or equal to 30kph
|
||||||
|
* - Is a [living street](https://wiki.openstreetmap.org/wiki/Tag:highway%3Dliving_street)
|
||||||
|
* - Is a separated cycleway
|
||||||
|
* - Is a cycle lane separated from the road
|
||||||
|
* - Is a shared path (bikes + pedestrians allowed)
|
||||||
|
* @param feature
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isGreenRoad(feature: Feature<Geometry, GeoJsonProperties>): boolean {
|
||||||
|
const p = feature.properties;
|
||||||
|
if (p === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (p.maxspeed <= 30) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.highway === 'cycleway') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.highway === 'shared_lane') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.bicycle === 'designated' && p.highway === 'cycleway') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.highway === 'living_street') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.cycleway === 'track' || p['cycleway:left'] === 'track' || p['cycleway:right'] === 'track') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -23,14 +23,17 @@ export const safeCycleways = (boundsStr: string) => `
|
||||||
/* Select road types to display */
|
/* Select road types to display */
|
||||||
|
|
||||||
(
|
(
|
||||||
way[highway]["highway"!~"cycleway|path|footway|pedestrian"]["bicycle"!~"no"][maxspeed](if:t["maxspeed"]<=50);
|
way[highway];
|
||||||
|
way["highway"="residential"];
|
||||||
|
|
||||||
way[highway=cycleway];
|
way[highway=cycleway];
|
||||||
way["highway"~"cycleway|path|footway|pedestrian"]["bicycle"~"yes|designated"];
|
way["highway"~"cycleway|path|footway|pedestrian"]["bicycle"~"yes|designated"];
|
||||||
way[highway=proposed][proposed=cycleway];
|
way[highway=proposed][proposed=cycleway];
|
||||||
way[highway=construction][construction=cycleway];
|
way[highway=construction][construction=cycleway];
|
||||||
way[proposed=cycleway];
|
way[proposed=cycleway];
|
||||||
|
way[cycleway=lane];
|
||||||
|
way["cycleway:left"=track];
|
||||||
|
way[cycleway=track];
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue