Porównaj commity

...

12 Commity

Autor SHA1 Wiadomość Data
Jake Coppinger 0234c6a747 Improve safety notice 2023-01-28 14:37:33 +11:00
Jake Coppinger 0c13b5b909 Fix zoom check at start 2023-01-28 14:18:07 +11:00
Jake Coppinger 68cd11a980 Improve description in top right 2023-01-28 14:10:37 +11:00
Jake Coppinger e484e23299 Remove old parking code, improve red routes 2023-01-28 13:58:29 +11:00
Jake Coppinger a7110eef7f Improve loading notifications 2023-01-28 13:48:20 +11:00
Jake Coppinger 7fbd8ac1d3 Add docstrings for selectors, improve selectors 2023-01-28 13:26:42 +11:00
Jake Coppinger 66a3ad80f3 Refactor selectors 2023-01-28 13:14:08 +11:00
Jake Coppinger 8f4446b462 Refactor road selectors 2023-01-28 13:11:46 +11:00
Jake Coppinger 865be63b88 More road filters into new file 2023-01-28 13:10:10 +11:00
Jake Coppinger 9861006cf1 Set loading status, improve road colours 2023-01-28 13:07:24 +11:00
Jake Coppinger 26feb770aa Draw orange as well. Still some roads missing 2023-01-28 12:28:23 +11:00
Jake Coppinger 751eae09ab Removing layers works 2023-01-27 20:28:51 +11:00
10 zmienionych plików z 281 dodań i 123 usunięć

Wyświetl plik

@ -1,8 +1,26 @@
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)
@ -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
osm2streets (which is written in Rust).
# Local development
See instructions for setting up the backend tileserve at

32
docs/key.md 100644
Wyświetl plik

@ -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.

Wyświetl plik

@ -104,7 +104,7 @@ h1 {
bottom: 15px;
}
.mapboxgl-ctrl-top-left {
margin-top: 50px;
margin-top: 120px;
}
.mapboxgl-ctrl-geocoder--icon.mapboxgl-ctrl-geocoder--icon-search {

Wyświetl plik

@ -1,10 +1,9 @@
import debounce from "debounce";
import { OverpassResponse, RawOverpassNode } from "./interfaces";
import { LoadingStatusType, OverpassResponse} from "./interfaces";
import * as http from "https";
import { drawMarkersAndCards, removeMarkers } from "./drawing";
import { wayToNode } from "./geo-utils";
import { bicycleParking, safeCycleways } from "./overpass-requests";
import { addStreetLayers, removeStreetLayers } from "./drawing";
import { safeCycleways } from "./overpass-requests";
import osmtogeojson from 'osmtogeojson';
@ -14,8 +13,10 @@ import osmtogeojson from 'osmtogeojson';
* @returns
*/
export async function getOSMData(overpassQuery: string): Promise<OverpassResponse> {
// overpass.kumi.systems
// hostname: "overpass-api.de",
const options = {
hostname: "overpass-api.de",
hostname: "overpass.kumi.systems",
port: 443,
path: "/api/interpreter",
method: "POST",
@ -64,17 +65,14 @@ async function fetchAndDrawMarkers(
const northLat = bounds.getNorth();
const eastLong = bounds.getEast();
let ads: OverpassResponse;
let safeRoutes: OverpassResponse;
const overpassBounds = [southernLat, westLong, northLat, eastLong];
const boundsStr = overpassBounds.join(",");
const parkingOverpassQuery = bicycleParking(boundsStr);;
const safeRoutesOverpassQuery = safeCycleways(boundsStr);;
console.log("Started POST request...");
try {
ads = (await getOSMData(parkingOverpassQuery));
safeRoutes = await getOSMData(safeRoutesOverpassQuery);
} catch (e) {
console.log("Error:", e);
@ -86,71 +84,8 @@ async function fetchAndDrawMarkers(
console.log(geoJson);
console.log("Adding geojson to map...");
// try {
// map.removeSource('greenRoads');
// } 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);
removeStreetLayers(map);
addStreetLayers(map, geoJson);
setLoadingStatus("success");
}
type LoadingStatusType = "loading" | "success" | "429error" | "unknownerror";

Wyświetl plik

@ -3,6 +3,8 @@ import {
RawOverpassNode,
} from "./interfaces";
import { FeatureCollection, Geometry, GeoJsonProperties, Feature, GeometryObject } from 'geojson';
import { isGreenRoad, isOrangeRoad, isRedRoad } from "./osm-selectors";
export function drawMarkerAndCard(
item: RawOverpassNode,
@ -53,6 +55,91 @@ export function drawMarkerAndCard(
export function removeMarkers(markers: mapboxgl.Marker[]): void {
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(
map: mapboxgl.Map,

Wyświetl plik

@ -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"

Wyświetl plik

@ -1,5 +1,6 @@
// TODO: Find the layer of the road labels for the maptiler background
const layerToAddAfter = undefined;
// const layerToAddAfter = 'greenRoadsId'; //undefined;
function addLayer(
map: mapboxgl.Map,

Wyświetl plik

@ -15,13 +15,17 @@ import { LoadingStatusType } from "./interfaces";
const MAPBOX_TOKEN =
"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;
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");
useState<LoadingStatusType>("ready_to_load");
const [lng, setLng] = useState(151.2160755932166);
const [lat, setLat] = useState(-33.88056647217827);
@ -41,29 +45,7 @@ export function Map() {
center: [lng, lat],
zoom: zoom,
hash: true,
style: {
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,
},
],
},
style: "mapbox://styles/mapbox/dark-v11",
});
const map = mapRef.current;
@ -78,12 +60,12 @@ export function Map() {
}),
"top-left"
);
map.addControl(
new MapboxDirections({
accessToken: mapboxgl.accessToken,
}),
"top-left"
);
// map.addControl(
// new MapboxDirections({
// accessToken: mapboxgl.accessToken,
// }),
// "top-left"
// );
map.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
@ -99,55 +81,70 @@ export function Map() {
}
const { lng, lat } = map.getCenter();
const zoom = map.getZoom();
if (zoom < min_overpass_turbo_zoom) {
setLoadingStatus("too_zoomed_out");
} else {
setLoadingStatus("ready_to_load");
}
console.log(lng, lat, zoom);
setLng(map.getCenter().lng);
setLat(map.getCenter().lat);
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 () => {
if (map === null) {
return;
}
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
const zoom = map.getZoom();
if (zoom > min_overpass_turbo_zoom) {
console.log(`zoom is ${zoom}`);
debouncedFetchAndDrawMarkers(map, markers, setLoadingStatus);
}
});
});
const statusMessages = {
loading: "Loading from OpenStreetMap...",
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",
};
const statusText = statusMessages[loadingStatus];
return (
<div>
<div className="sidebar">
<label>
{" "}
{statusText} |{" "}
A work in progress side project by{" "}
<span color="red">Warning:</span> Data is open source and not guaranteed to be
accurate.
<br></br>
<a
target="_blank"
rel="noopener noreferrer"
href="https://jakecoppinger.com/"
href="https://github.com/jakecoppinger/safe-cycling-map/blob/main/key.md"
>
Jake Coppinger
</a>{" "}
| Open source on{" "}
View map key and how safety is calculated
</a>
<br></br>
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/jakecoppinger/safe-cycling-map"
>
Github
About this map
</a>
<br></br>
{statusText}
</label>
</div>
<div ref={mapContainer} className="map-container" />

Wyświetl plik

@ -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
}

Wyświetl plik

@ -23,14 +23,17 @@ export const safeCycleways = (boundsStr: string) => `
/* 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|path|footway|pedestrian"]["bicycle"~"yes|designated"];
way[highway=proposed][proposed=cycleway];
way[highway=construction][construction=cycleway];
way[proposed=cycleway];
way[cycleway=lane];
way["cycleway:left"=track];
way[cycleway=track];
);