Merge pull request #242 from openbikesensor/rural_urban

Implement difference between urban and rural for events and roads
This commit is contained in:
gluap 2022-07-26 07:58:50 +02:00 committed by GitHub
commit 2755d6b2b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 134 additions and 31 deletions

View file

@ -112,6 +112,7 @@ async def mapdetails_road(req):
"histogram": {
"bins": [None if math.isinf(b) else b for b in bins.tolist()],
"counts": hist.tolist(),
"zone": road.zone
},
"values": list(map(rounder, arr.tolist())),
}

View file

@ -44,27 +44,74 @@ export const reds = [
]
export function colorByCount(attribute = 'event_count', maxCount, colormap = viridis) {
return colormapToScale(colormap, ['case', ['to-boolean', ['get', attribute]], ['get', attribute], 0], 0, maxCount)
return colormapToScale(colormap, ['case', isValidAttribute(attribute), ['get', attribute], 0], 0, maxCount)
}
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
var steps = {'rural': [1.6,1.8,2.0,2.2],
'urban': [1.1,1.3,1.5,1.7]}
export function isValidAttribute(attribute) {
if (attribute.endsWith('zone')) {
return ['in', ['get', attribute], ['literal', ['rural', 'urban']]]
}
return ['to-boolean', ['get', attribute]]
}
export function borderByZone() {
return ["match", ['get', 'zone'],
"rural", "cyan",
"urban", "blue",
"purple"
]
}
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC', zone='urban') {
return [
'case',
['!', ['to-boolean', ['get', attribute]]],
['!', isValidAttribute(attribute)],
fallback,
["match", ['get', 'zone'], "rural",
[
'step',
['get', attribute],
'rgba(150, 0, 0, 1)',
1.1,
steps['rural'][0],
'rgba(255, 0, 0, 1)',
1.3,
steps['rural'][1],
'rgba(255, 220, 0, 1)',
1.5,
steps['rural'][2],
'rgba(67, 200, 0, 1)',
1.7,
steps['rural'][3],
'rgba(67, 150, 0, 1)',
], "urban",
[
'step',
['get', attribute],
'rgba(150, 0, 0, 1)',
steps['urban'][0],
'rgba(255, 0, 0, 1)',
steps['urban'][1],
'rgba(255, 220, 0, 1)',
steps['urban'][2],
'rgba(67, 200, 0, 1)',
steps['urban'][3],
'rgba(67, 150, 0, 1)',
],
[
'step',
['get', attribute],
'rgba(150, 0, 0, 1)',
steps['urban'][0],
'rgba(255, 0, 0, 1)',
steps['urban'][1],
'rgba(255, 220, 0, 1)',
steps['urban'][2],
'rgba(67, 200, 0, 1)',
steps['urban'][3],
'rgba(67, 150, 0, 1)',
]
]
]
}

View file

@ -1,7 +1,7 @@
import React from 'react'
import _ from 'lodash'
import {connect} from 'react-redux'
import {List, Select, Input, Divider, Checkbox, Header} from 'semantic-ui-react'
import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react'
import {
MapConfig,
@ -23,6 +23,7 @@ const ROAD_ATTRIBUTE_OPTIONS = [
{value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'},
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'},
{value: 'usage_count', key: 'usage_count', text: 'Usage count'},
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'}
]
function LayerSidebar({
@ -96,10 +97,24 @@ function LayerSidebar({
<ColorMapLegend map={_.chunk(colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml ).slice(3), 2)} twoTicks />
</List.Item></>
) :
(
attribute.endsWith('zone') ? (
<>
<List.Item>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3].slice(2)} />
<Label size="small" style={{background: "blue",color:"white"}}>urban (1.5&nbsp;m)</Label>
<Label size="small" style={{background: "cyan", color:"black"}}>rural (2&nbsp;m)</Label>
</List.Item></>
) :
(
<>
<List.Item>
<List.Header>Urban</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item>
<List.Item>
<List.Header>Rural</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item>
</>
)}
</>
)}
@ -120,7 +135,12 @@ function LayerSidebar({
{showEvents && (
<>
<List.Item>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3].slice(2)} />
<List.Header>Urban</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item>
<List.Item>
<List.Header>Rural</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item>
</>
)}

View file

@ -9,7 +9,7 @@ import {Chart} from 'components'
import {pairwise} from 'utils'
import api from 'api'
import {colorByDistance} from 'mapstyles'
import {colorByDistance, borderByZone} from 'mapstyles'
import styles from './styles.module.less'
@ -34,7 +34,7 @@ const LABELS = {
max: 'Maximum',
mean: 'Average',
}
const ZONE_COLORS = {urban: 'olive', rural: 'brown', motorway: 'purple'}
const ZONE_COLORS = {urban: 'blue', rural: 'cyan', motorway: 'purple'}
const CARDINAL_DIRECTIONS = ['north', 'north-east', 'east', 'south-east', 'south', 'south-west', 'west', 'north-west']
const getCardinalDirection = (bearing) =>
bearing == null
@ -75,14 +75,15 @@ function RoadStatsTable({data}) {
)
}
function HistogramChart({bins, counts}) {
function HistogramChart({bins, counts, zone}) {
const diff = bins[1] - bins[0]
const colortype = zone=="rural" ? 3:5;
const data = _.zip(
bins.slice(0, bins.length - 1).map((v) => v + diff / 2),
counts
).map((value) => ({
value,
itemStyle: {color: selectFromColorMap(colorByDistance()[3].slice(2), value[0])},
itemStyle: {color: selectFromColorMap(colorByDistance()[3][colortype].slice(2), value[0]),},
}))
return (

View file

@ -6,7 +6,7 @@ import produce from 'immer'
import {Page, Map} from 'components'
import {useConfig} from 'config'
import {colorByDistance, colorByCount, reds} from 'mapstyles'
import {colorByDistance, colorByCount, borderByZone, reds, isValidAttribute} from 'mapstyles'
import {useMapConfig} from 'reducers/mapConfig'
import RoadInfo from './RoadInfo'
@ -40,20 +40,23 @@ const untaggedRoadsLayer = {
minzoom: 12,
}
const getUntaggedRoadsLayer = (colorAttribute, maxCount) =>
produce(untaggedRoadsLayer, (draft) => {
draft.filter = ['!', isValidAttribute(colorAttribute)]
})
const getRoadsLayer = (colorAttribute, maxCount) =>
produce(untaggedRoadsLayer, (draft) => {
draft.id = 'obs_roads_normal'
if (colorAttribute.endsWith('_count')) {
// delete draft.filter
draft.filter = ['to-boolean', ['get', colorAttribute]]
} else {
draft.filter = draft.filter[1] // remove '!'
}
draft.filter = isValidAttribute(colorAttribute)
draft.paint['line-width'][6] = 6 // scale bigger on zoom
draft.paint['line-color'] = colorAttribute.startsWith('distance_')
? colorByDistance(colorAttribute)
: colorAttribute.endsWith('_count')
? colorByCount(colorAttribute, maxCount)
: colorAttribute.endsWith('zone')
? borderByZone()
: '#DDD'
draft.paint['line-opacity'][3] = 12
draft.paint['line-opacity'][5] = 13
@ -128,8 +131,9 @@ export default function MapPage() {
const layers = []
const untaggedRoadsLayerCustom = useMemo(() => getUntaggedRoadsLayer(attribute), [attribute])
if (mapConfig.obsRoads.show && mapConfig.obsRoads.showUntagged) {
layers.push(untaggedRoadsLayer)
layers.push(untaggedRoadsLayerCustom)
}
const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount])

View file

@ -11,7 +11,8 @@ type RoadAttribute =
| "distance_overtaker_max"
| "distance_overtaker_median"
| "overtaking_event_count"
| "usage_count";
| "usage_count"
| "zone";
export type MapConfig = {
baseMap: {

View file

@ -163,6 +163,7 @@ module.exports = function (webpackEnv) {
'/config.json': apiUrl,
'/api': apiUrl,
'/login': apiUrl,
'/tiles': apiUrl
},
},
module: {

View file

@ -61,6 +61,8 @@ local roads = osm2pgsql.define_way_table('road', {
{ column = 'oneway', type = 'bool' },
})
local minspeed_rural = 60
function osm2pgsql.process_way(object)
if object.tags.highway and contains(HIGHWAY_TYPES, object.tags.highway) then
local tags = object.tags
@ -81,13 +83,30 @@ function osm2pgsql.process_way(object)
zone = "urban"
elseif string.match(zone, "motorway") then
zone = "motorway"
elseif contains(URBAN_TYPES, tags.highway) then
elseif string.match(zone, "30") then
zone = "urban"
else
zone = "urban"
end
end
if not tags["zone:traffic"] then
if contains(URBAN_TYPES, tags.highway) then
zone = "urban"
elseif contains(MOTORWAY_TYPES, tags.highway) then
zone = "motorway"
elseif (tags.maxspeed) and (tonumber(string.match(tags.maxspeed, '[%d]*'))) and tonumber(string.match(tags.maxspeed, '[%d]*')) > minspeed_rural then
zone = "rural"
elseif (tags["maxspeed:forward"]) and (tonumber(string.match(tags["maxspeed:forward"], '[%d]*'))) and tonumber(string.match(tags["maxspeed:forward"], '[%d]*')) > minspeed_rural then
zone = "rural"
elseif (tags["maxspeed:backward"]) and (tonumber(string.match(tags["maxspeed:backward"], '[%d]*'))) and tonumber(string.match(tags["maxspeed:backward"], '[%d]*')) > minspeed_rural then
zone = "rural"
elseif tags['source:maxspeed'] and string.match(tags['source:maxspeed'], "rural") then
zone = "rural"
elseif tags['source:maxspeed'] and string.match(tags['source:maxspeed'], "urban") then
zone = "urban"
else
-- we can't figure it out
zone = nil
zone = "urban"
end
end

View file

@ -1,5 +1,5 @@
CREATE OR REPLACE FUNCTION layer_obs_events(bbox geometry, zoom_level int)
RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, distance_stationary float, direction int, course float, speed float, way_id bigint) AS $$
RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, distance_stationary float, direction int, course float, speed float, zone zone_type, way_id bigint) AS $$
SELECT
id::bigint as event_id,
@ -9,8 +9,10 @@ RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, dist
(case when direction_reversed then -1 else 1 end)::int as direction,
course,
speed,
way_id::bigint as way_id
CASE WHEN road.zone IS NULL THEN 'urban' else road.zone END as zone,
overtaking_event.way_id::bigint as way_id
FROM overtaking_event
FULL OUTER JOIN road ON (road.way_id = overtaking_event.way_id)
WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox;
$$ LANGUAGE SQL IMMUTABLE;

View file

@ -16,6 +16,8 @@ layer:
Direction of travel, as reported by GPS, in degree from North.
speed: |
Speed of travel, as reported by GPS, in meters per second (?).
zone: |
rural or urban
defaults:
srs: EPSG:3785
datasource:
@ -23,7 +25,7 @@ layer:
geometry_field: geometry
key_field: event_id
key_field_as_attribute: no
query: (SELECT event_id, geometry, distance_overtaker, distance_stationary, direction, course, speed, way_id FROM layer_obs_events(!bbox!, z(!scale_denominator!))) AS t
query: (SELECT event_id, geometry, distance_overtaker, distance_stationary, direction, course, speed, zone, way_id FROM layer_obs_events(!bbox!, z(!scale_denominator!))) AS t
schema:
- ./layer.sql

View file

@ -12,6 +12,7 @@ RETURNS TABLE(
overtaking_event_count int,
usage_count bigint,
direction int,
zone zone_type,
offset_direction int
) AS $$
@ -27,12 +28,13 @@ RETURNS TABLE(
(select count(id) from road_usage where road_usage.way_id = road.way_id and
(road.directionality != 0 or road_usage.direction_reversed = r.rev)) as usage_count,
r.dir as direction,
road.zone::zone_type as zone,
case when road.directionality = 0 then r.dir else 0 end as offset_direction
FROM road
LEFT JOIN (VALUES (-1, TRUE), (1, FALSE), (0, FALSE)) AS r(dir, rev) ON (abs(r.dir) != road.directionality)
FULL OUTER JOIN overtaking_event ON (road.way_id = overtaking_event.way_id and (road.directionality != 0 or overtaking_event.direction_reversed = r.rev))
-- WHERE road.name = 'Merzhauser Straße'
WHERE road.geometry && bbox
GROUP BY road.name, road.way_id, road.geometry, road.directionality, r.dir, r.rev;
GROUP BY road.name, road.way_id, road.geometry, road.directionality, r.dir, r.rev, road.zone;
$$ LANGUAGE SQL IMMUTABLE;

View file

@ -22,6 +22,8 @@ layer:
Contains -1 for events while going along the way backwards, 1 for
forwards. Each road is emitted twice, if it has data for both directions,
even if it is oneway.
zone: |
ural or urban
offset_direction: |
Factor for offset to shift the line to the driving side. One of -1, 0, 1.
defaults:
@ -44,6 +46,7 @@ layer:
overtaking_event_count,
usage_count,
direction,
zone,
offset_direction
FROM layer_obs_roads(!bbox!, z(!scale_denominator!))
) AS t