diff --git a/api/obs/api/routes/mapdetails.py b/api/obs/api/routes/mapdetails.py index 51aa7df..5b0eba3 100644 --- a/api/obs/api/routes/mapdetails.py +++ b/api/obs/api/routes/mapdetails.py @@ -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())), } diff --git a/frontend/src/mapstyles/index.js b/frontend/src/mapstyles/index.js index c3a62fb..30067d0 100644 --- a/frontend/src/mapstyles/index.js +++ b/frontend/src/mapstyles/index.js @@ -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)', + ] + ] ] } diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index 9e38026..46167b2 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -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({ ) : - ( + attribute.endsWith('zone') ? ( + <> - - + + + + ) : + ( + <> + + Urban + + + + Rural + + + )} )} @@ -120,7 +135,12 @@ function LayerSidebar({ {showEvents && ( <> - + Urban + + + + Rural + )} diff --git a/frontend/src/pages/MapPage/RoadInfo.tsx b/frontend/src/pages/MapPage/RoadInfo.tsx index 5d2758e..871279a 100644 --- a/frontend/src/pages/MapPage/RoadInfo.tsx +++ b/frontend/src/pages/MapPage/RoadInfo.tsx @@ -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 ( diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx index 9d30847..a3196dd 100644 --- a/frontend/src/pages/MapPage/index.tsx +++ b/frontend/src/pages/MapPage/index.tsx @@ -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]) diff --git a/frontend/src/reducers/mapConfig.ts b/frontend/src/reducers/mapConfig.ts index f0140cb..e7c14cf 100644 --- a/frontend/src/reducers/mapConfig.ts +++ b/frontend/src/reducers/mapConfig.ts @@ -11,7 +11,8 @@ type RoadAttribute = | "distance_overtaker_max" | "distance_overtaker_median" | "overtaking_event_count" - | "usage_count"; + | "usage_count" + | "zone"; export type MapConfig = { baseMap: { diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 0930c34..3206622 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -163,6 +163,7 @@ module.exports = function (webpackEnv) { '/config.json': apiUrl, '/api': apiUrl, '/login': apiUrl, + '/tiles': apiUrl }, }, module: { diff --git a/roads_import.lua b/roads_import.lua index c314d50..2c1c45b 100644 --- a/roads_import.lua +++ b/roads_import.lua @@ -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 diff --git a/tile-generator/layers/obs_events/layer.sql b/tile-generator/layers/obs_events/layer.sql index c57874a..7ae714f 100644 --- a/tile-generator/layers/obs_events/layer.sql +++ b/tile-generator/layers/obs_events/layer.sql @@ -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; diff --git a/tile-generator/layers/obs_events/obs_events.yaml b/tile-generator/layers/obs_events/obs_events.yaml index 4eb82d2..be11e7c 100644 --- a/tile-generator/layers/obs_events/obs_events.yaml +++ b/tile-generator/layers/obs_events/obs_events.yaml @@ -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 diff --git a/tile-generator/layers/obs_roads/layer.sql b/tile-generator/layers/obs_roads/layer.sql index 92c4f2f..53d8dd3 100644 --- a/tile-generator/layers/obs_roads/layer.sql +++ b/tile-generator/layers/obs_roads/layer.sql @@ -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; diff --git a/tile-generator/layers/obs_roads/obs_roads.yaml b/tile-generator/layers/obs_roads/obs_roads.yaml index b246eb4..3b0e918 100644 --- a/tile-generator/layers/obs_roads/obs_roads.yaml +++ b/tile-generator/layers/obs_roads/obs_roads.yaml @@ -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