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": { "histogram": {
"bins": [None if math.isinf(b) else b for b in bins.tolist()], "bins": [None if math.isinf(b) else b for b in bins.tolist()],
"counts": hist.tolist(), "counts": hist.tolist(),
"zone": road.zone
}, },
"values": list(map(rounder, arr.tolist())), "values": list(map(rounder, arr.tolist())),
} }

View file

@ -44,27 +44,74 @@ export const reds = [
] ]
export function colorByCount(attribute = 'event_count', maxCount, colormap = viridis) { 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 [ return [
'case', 'case',
['!', ['to-boolean', ['get', attribute]]], ['!', isValidAttribute(attribute)],
fallback, fallback,
["match", ['get', 'zone'], "rural",
[ [
'step', 'step',
['get', attribute], ['get', attribute],
'rgba(150, 0, 0, 1)', 'rgba(150, 0, 0, 1)',
1.1, steps['rural'][0],
'rgba(255, 0, 0, 1)', 'rgba(255, 0, 0, 1)',
1.3, steps['rural'][1],
'rgba(255, 220, 0, 1)', 'rgba(255, 220, 0, 1)',
1.5, steps['rural'][2],
'rgba(67, 200, 0, 1)', '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)', '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 React from 'react'
import _ from 'lodash' import _ from 'lodash'
import {connect} from 'react-redux' 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 { import {
MapConfig, MapConfig,
@ -23,6 +23,7 @@ const ROAD_ATTRIBUTE_OPTIONS = [
{value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'}, {value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'},
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'}, {value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'},
{value: 'usage_count', key: 'usage_count', text: 'Usage count'}, {value: 'usage_count', key: 'usage_count', text: 'Usage count'},
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'}
] ]
function LayerSidebar({ function LayerSidebar({
@ -96,10 +97,24 @@ function LayerSidebar({
<ColorMapLegend map={_.chunk(colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml ).slice(3), 2)} twoTicks /> <ColorMapLegend map={_.chunk(colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml ).slice(3), 2)} twoTicks />
</List.Item></> </List.Item></>
) : ) :
( attribute.endsWith('zone') ? (
<>
<List.Item> <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.Item>
<List.Header>Rural</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item>
</>
)} )}
</> </>
)} )}
@ -120,7 +135,12 @@ function LayerSidebar({
{showEvents && ( {showEvents && (
<> <>
<List.Item> <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> </List.Item>
</> </>
)} )}

View file

@ -9,7 +9,7 @@ import {Chart} from 'components'
import {pairwise} from 'utils' import {pairwise} from 'utils'
import api from 'api' import api from 'api'
import {colorByDistance} from 'mapstyles' import {colorByDistance, borderByZone} from 'mapstyles'
import styles from './styles.module.less' import styles from './styles.module.less'
@ -34,7 +34,7 @@ const LABELS = {
max: 'Maximum', max: 'Maximum',
mean: 'Average', 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 CARDINAL_DIRECTIONS = ['north', 'north-east', 'east', 'south-east', 'south', 'south-west', 'west', 'north-west']
const getCardinalDirection = (bearing) => const getCardinalDirection = (bearing) =>
bearing == null 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 diff = bins[1] - bins[0]
const colortype = zone=="rural" ? 3:5;
const data = _.zip( const data = _.zip(
bins.slice(0, bins.length - 1).map((v) => v + diff / 2), bins.slice(0, bins.length - 1).map((v) => v + diff / 2),
counts counts
).map((value) => ({ ).map((value) => ({
value, value,
itemStyle: {color: selectFromColorMap(colorByDistance()[3].slice(2), value[0])}, itemStyle: {color: selectFromColorMap(colorByDistance()[3][colortype].slice(2), value[0]),},
})) }))
return ( return (

View file

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

View file

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

View file

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

View file

@ -61,6 +61,8 @@ local roads = osm2pgsql.define_way_table('road', {
{ column = 'oneway', type = 'bool' }, { column = 'oneway', type = 'bool' },
}) })
local minspeed_rural = 60
function osm2pgsql.process_way(object) function osm2pgsql.process_way(object)
if object.tags.highway and contains(HIGHWAY_TYPES, object.tags.highway) then if object.tags.highway and contains(HIGHWAY_TYPES, object.tags.highway) then
local tags = object.tags local tags = object.tags
@ -81,13 +83,30 @@ function osm2pgsql.process_way(object)
zone = "urban" zone = "urban"
elseif string.match(zone, "motorway") then elseif string.match(zone, "motorway") then
zone = "motorway" 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" zone = "urban"
elseif contains(MOTORWAY_TYPES, tags.highway) then elseif contains(MOTORWAY_TYPES, tags.highway) then
zone = "motorway" 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 else
-- we can't figure it out -- we can't figure it out
zone = nil zone = "urban"
end end
end end

View file

@ -1,5 +1,5 @@
CREATE OR REPLACE FUNCTION layer_obs_events(bbox geometry, zoom_level int) 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 SELECT
id::bigint as event_id, 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, (case when direction_reversed then -1 else 1 end)::int as direction,
course, course,
speed, 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 FROM overtaking_event
FULL OUTER JOIN road ON (road.way_id = overtaking_event.way_id)
WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox; WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox;
$$ LANGUAGE SQL IMMUTABLE; $$ LANGUAGE SQL IMMUTABLE;

View file

@ -16,6 +16,8 @@ layer:
Direction of travel, as reported by GPS, in degree from North. Direction of travel, as reported by GPS, in degree from North.
speed: | speed: |
Speed of travel, as reported by GPS, in meters per second (?). Speed of travel, as reported by GPS, in meters per second (?).
zone: |
rural or urban
defaults: defaults:
srs: EPSG:3785 srs: EPSG:3785
datasource: datasource:
@ -23,7 +25,7 @@ layer:
geometry_field: geometry geometry_field: geometry
key_field: event_id key_field: event_id
key_field_as_attribute: no 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: schema:
- ./layer.sql - ./layer.sql

View file

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

View file

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