Compare commits

...

4 commits

Author SHA1 Message Date
Paul Bienkowski bf740a0695 wip:Road.has_cars 2022-04-05 17:51:39 +02:00
Paul Bienkowski 9d97489ac9 Ignore inaccessible roads, certain service roads, and areas (fixes #116) 2022-04-03 19:40:57 +02:00
Paul Bienkowski 73c1a62f80 Ignore link-type highways (usually turning lanes) during import 2022-04-03 19:40:09 +02:00
Paul Bienkowski 22ae2c08a6 Add administrative boundary import and display regional event count 2022-04-03 18:10:29 +02:00
15 changed files with 312 additions and 56 deletions

View file

@ -0,0 +1,36 @@
"""create table region
Revision ID: a049e5eb24dd
Revises: a9627f63fbed
Create Date: 2022-04-02 21:28:43.124521
"""
from alembic import op
import sqlalchemy as sa
from migrations.utils import dbtype
# revision identifiers, used by Alembic.
revision = "a049e5eb24dd"
down_revision = "a9627f63fbed"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"region",
sa.Column(
"way_id", sa.BIGINT, autoincrement=True, primary_key=True, index=True
),
sa.Column("zone", dbtype("zone_type")),
sa.Column("name", sa.String),
sa.Column("geometry", dbtype("GEOMETRY"), index=True),
sa.Column("directionality", sa.Integer),
sa.Column("oenway", sa.Boolean),
)
def downgrade():
op.drop_table("region")

View file

@ -0,0 +1,24 @@
"""add road column has_cars
Revision ID: ddde1cdc767b
Revises: a049e5eb24dd
Create Date: 2022-04-03 20:13:22.874195
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ddde1cdc767b"
down_revision = "a049e5eb24dd"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("road", sa.Column("has_cars", sa.Boolean))
def downgrade():
op.drop_column("road", "has_cars")

View file

@ -26,7 +26,7 @@ if app.config.FRONTEND_CONFIG:
.replace("111", "{x}") .replace("111", "{x}")
.replace("222", "{y}") .replace("222", "{y}")
], ],
"minzoom": 12, "minzoom": 0,
"maxzoom": 14, "maxzoom": 14,
} }
), ),

View file

@ -12,7 +12,7 @@
"obsMapSource": { "obsMapSource": {
"type": "vector", "type": "vector",
"tiles": ["https://portal.example.com/tiles/{z}/{x}/{y}.pbf"], "tiles": ["https://portal.example.com/tiles/{z}/{x}/{y}.pbf"],
"minzoom": 12, "minzoom": 0,
"maxzoom": 14 "maxzoom": 14
} }
} }

View file

@ -59,7 +59,7 @@ export function DiscreteColorMapLegend({map}: {map: ColorMap}) {
) )
} }
export default function ColorMapLegend({map, twoTicks = false}: {map: ColorMap, twoTicks?: boolean}) { export default function ColorMapLegend({map, twoTicks = false, digits=2}: {map: ColorMap, twoTicks?: boolean, digits?: number}) {
const min = map[0][0] const min = map[0][0]
const max = map[map.length - 1][0] const max = map[map.length - 1][0]
const normalizeValue = (v) => (v - min) / (max - min) const normalizeValue = (v) => (v - min) / (max - min)
@ -81,7 +81,7 @@ export default function ColorMapLegend({map, twoTicks = false}: {map: ColorMap,
</svg> </svg>
{tickValues.map(([value]) => ( {tickValues.map(([value]) => (
<span className={styles.tick} key={value} style={{left: normalizeValue(value) * 100 + '%'}}> <span className={styles.tick} key={value} style={{left: normalizeValue(value) * 100 + '%'}}>
{value.toFixed(2)} {value.toFixed(digits)}
</span> </span>
))} ))}
</div> </div>

View file

@ -76,6 +76,58 @@ export const trackLayer = {
}, },
} }
export const getRegionLayers = (adminLevel = 6, baseColor = "#00897B", maxValue = 5000) => [{
id: 'region',
"type": "fill",
"source": "obs",
"source-layer": "obs_regions",
"minzoom": 0,
"maxzoom": 10,
"filter": [
"all",
["==", "admin_level", adminLevel]
],
"paint": {
"fill-color": baseColor,
"fill-antialias": true,
"fill-opacity": [
"interpolate",
["linear"],
[
"log10",
[
"get",
"overtaking_event_count"
]
],
0,
0,
Math.log10(maxValue),
0.9
]
},
},
{
id: 'region-border',
"type": "line",
"source": "obs",
"source-layer": "obs_regions",
"minzoom": 0,
"maxzoom": 10,
"filter": [
"all",
["==", "admin_level", adminLevel]
],
"paint": {
"line-width": 1,
"line-color": baseColor,
},
"layout": {
"line-join": "round",
"line-cap": "round"
}
}]
export const trackLayerRaw = produce(trackLayer, draft => { export const trackLayerRaw = produce(trackLayer, draft => {
draft.paint['line-color'] = '#81D4FA' draft.paint['line-color'] = '#81D4FA'
draft.paint['line-width'][4] = 1 draft.paint['line-width'][4] = 1

View file

@ -36,6 +36,7 @@ function LayerSidebar({
baseMap: {style}, baseMap: {style},
obsRoads: {show: showRoads, showUntagged, attribute, maxCount}, obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
obsEvents: {show: showEvents}, obsEvents: {show: showEvents},
obsRegions: {show: showRegions},
} = mapConfig } = mapConfig
return ( return (
@ -50,6 +51,28 @@ function LayerSidebar({
/> />
</List.Item> </List.Item>
<Divider /> <Divider />
<List.Item>
<Checkbox
toggle
size="small"
id="obsRegions.show"
style={{float: 'right'}}
checked={showRegions}
onChange={() => setMapConfigFlag('obsRegions.show', !showRegions)}
/>
<label htmlFor="obsRegions.show">
<Header as="h4">Regions</Header>
</label>
</List.Item>
{showRegions && (
<>
<List.Item>Color regions based on event count</List.Item>
<List.Item>
<ColorMapLegend twoTicks map={[[0, "#00897B00"], [5000, "#00897BFF"]]} digits={0} />
</List.Item>
</>
)}
<Divider />
<List.Item> <List.Item>
<Checkbox <Checkbox
toggle toggle

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, getRegionLayers} from 'mapstyles'
import {useMapConfig} from 'reducers/mapConfig' import {useMapConfig} from 'reducers/mapConfig'
import RoadInfo from './RoadInfo' import RoadInfo from './RoadInfo'
@ -18,6 +18,7 @@ const untaggedRoadsLayer = {
type: 'line', type: 'line',
source: 'obs', source: 'obs',
'source-layer': 'obs_roads', 'source-layer': 'obs_roads',
minzoom: 12,
filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]],
layout: { layout: {
'line-cap': 'round', 'line-cap': 'round',
@ -26,7 +27,7 @@ const untaggedRoadsLayer = {
paint: { paint: {
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2], 'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
'line-color': '#ABC', 'line-color': '#ABC',
'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1], // 'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1],
'line-offset': [ 'line-offset': [
'interpolate', 'interpolate',
['exponential', 1.5], ['exponential', 1.5],
@ -49,14 +50,15 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
} else { } else {
draft.filter = draft.filter[1] // remove '!' draft.filter = draft.filter[1] // remove '!'
} }
draft.minzoom = 10
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)
: '#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
}) })
const getEventsLayer = () => ({ const getEventsLayer = () => ({
@ -137,6 +139,9 @@ export default function MapPage() {
layers.push(roadsLayer) layers.push(roadsLayer)
} }
const regionLayers = useMemo(() => getRegionLayers(), [])
layers.push(...regionLayers)
const eventsLayer = useMemo(() => getEventsLayer(), []) const eventsLayer = useMemo(() => getEventsLayer(), [])
const eventsTextLayer = useMemo(() => getEventsTextLayer(), []) const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
if (mapConfig.obsEvents.show) { if (mapConfig.obsEvents.show) {

View file

@ -26,6 +26,9 @@ export type MapConfig = {
obsEvents: { obsEvents: {
show: boolean; show: boolean;
}; };
obsRegions: {
show: boolean;
};
}; };
export const initialState: MapConfig = { export const initialState: MapConfig = {
@ -41,6 +44,9 @@ export const initialState: MapConfig = {
obsEvents: { obsEvents: {
show: false, show: false,
}, },
obsRegions: {
show: true,
},
}; };
type MapConfigAction = { type MapConfigAction = {

View file

@ -26,15 +26,20 @@ local HIGHWAY_TYPES = {
"tertiary", "tertiary",
"unclassified", "unclassified",
"residential", "residential",
"trunk_link", -- "trunk_link",
"primary_link", -- "primary_link",
"secondary_link", -- "secondary_link",
"tertiary_link", -- "tertiary_link",
"living_street", "living_street",
"service", "service",
"track", "track",
"road", "road",
} }
local UNMOTORIZED_TRAFFIC_HIGHWAY_TYPES = {
"cycleway",
"footway",
"path",
}
local ZONE_TYPES = { local ZONE_TYPES = {
"urban", "urban",
"rural", "rural",
@ -47,68 +52,119 @@ local URBAN_TYPES = {
} }
local MOTORWAY_TYPES = { local MOTORWAY_TYPES = {
"motorway", "motorway",
"motorway_link", -- "motorway_link",
} }
local ADMIN_LEVEL_MIN = 2
local ADMIN_LEVEL_MAX = 8
local ONEWAY_YES = {"yes", "true", "1"} local ONEWAY_YES = {"yes", "true", "1"}
local ONEWAY_REVERSE = {"reverse", "-1"} local ONEWAY_REVERSE = {"reverse", "-1"}
-- https://wiki.openstreetmap.org/wiki/Tag:highway=service
local IGNORED_SERVICE_TYPES = {"parking_aisle", "driveway", "emergency_access", "drive-through"}
-- https://taginfo.openstreetmap.org/keys/access#values
local IGNORED_ACCESS_TYPES = {"no", "private", "permit", "official", "service", "emergency"}
local roads = osm2pgsql.define_way_table('road', { local roads = osm2pgsql.define_way_table('road', {
{ column = 'zone', type = 'text', sql_type="zone_type" }, { column = 'zone', type = 'text', sql_type="zone_type" },
{ column = 'directionality', type = 'int' }, { column = 'directionality', type = 'int' },
{ column = 'name', type = 'text' }, { column = 'name', type = 'text' },
{ column = 'geometry', type = 'linestring' }, { column = 'geometry', type = 'linestring' },
{ column = 'oneway', type = 'bool' }, { column = 'oneway', type = 'bool' },
{ column = 'has_cars', type = 'bool' },
}) })
local regions = osm2pgsql.define_relation_table('region', {
{ column = 'name', type = 'text' },
{ column = 'geometry', type = 'geometry' },
{ column = 'admin_level', type = 'int' },
{ column = 'tags', type = 'hstore' },
})
function osm2pgsql.process_way(object) function osm2pgsql.process_way(object)
if object.tags.highway and contains(HIGHWAY_TYPES, object.tags.highway) then local tags = object.tags
local tags = object.tags
local zone = nil
if tags["zone:traffic"] then -- only import certain highway ways, i.e. roads and pathways
zone = tags["zone:traffic"] if not tags.highway then return end
if zone == "DE:urban" then local unmotorized_traffic = contains(UNMOTORIZED_TRAFFIC_HIGHWAY_TYPES, tags.highway)
zone = "urban" if not contains(HIGHWAY_TYPES, tags.highway) and not unmotorized_traffic then return end
elseif zone == "DE:rural" then
zone = "rural" -- do not import areas (plazas etc.)
elseif zone == "DE:motorway" then if tags.area == "yes" then return end
zone = "motorway"
elseif string.match(zone, "rural") then -- ignore certain service roads
zone = "rural" if contains(IGNORED_SERVICE_TYPES, tags.service) then return end
elseif string.match(zone, "urban") then
zone = "urban" -- ignore disallowed roads (often tram tracks and similar)
elseif string.match(zone, "motorway") then if contains(IGNORED_ACCESS_TYPES, tags.access) then return end
zone = "motorway"
elseif contains(URBAN_TYPES, tags.highway) then local zone = nil
zone = "urban"
elseif contains(MOTORWAY_TYPES, tags.highway) then if tags["zone:traffic"] then
zone = "motorway" zone = tags["zone:traffic"]
else
-- we can't figure it out if zone == "DE:urban" then
zone = nil zone = "urban"
end elseif zone == "DE:rural" then
zone = "rural"
elseif zone == "DE:motorway" then
zone = "motorway"
elseif string.match(zone, "rural") then
zone = "rural"
elseif string.match(zone, "urban") then
zone = "urban"
elseif string.match(zone, "motorway") then
zone = "motorway"
elseif contains(URBAN_TYPES, tags.highway) then
zone = "urban"
elseif contains(MOTORWAY_TYPES, tags.highway) then
zone = "motorway"
else
-- we can't figure it out
zone = nil
end end
end
local directionality = 0 local directionality = 0
local oneway = tags.oneway local oneway = tags.oneway
-- See https://wiki.openstreetmap.org/wiki/Key:oneway section "Implied oneway restriction" -- See https://wiki.openstreetmap.org/wiki/Key:oneway section "Implied oneway restriction"
if contains(ONEWAY_YES, tags.oneway) or tags.junction == "roundabout" or zone == "motorway" then if contains(ONEWAY_YES, tags.oneway) or tags.junction == "roundabout" or zone == "motorway" then
directionality = 1 directionality = 1
oneway = true oneway = true
elseif contains(ONEWAY_REVERSE, tags.oneway) then elseif contains(ONEWAY_REVERSE, tags.oneway) then
directionality = -1 directionality = -1
oneway = true oneway = true
end end
roads:add_row({ roads:add_row({
geom = { create = 'linear' }, geom = { create = 'linear' },
name = tags.name, name = tags.name,
zone = zone, zone = zone,
directionality = directionality, directionality = directionality,
oneway = oneway, oneway = oneway,
has_cars = not unmotorized_traffic
})
end
function osm2pgsql.process_relation(object)
local admin_level = tonumber(object.tags.admin_level)
if object.tags.boundary == "administrative" and admin_level and admin_level >= ADMIN_LEVEL_MIN and admin_level <= ADMIN_LEVEL_MAX then
regions:add_row({
geometry = { create = 'area' },
name = object.tags.name,
admin_level = admin_level,
tags = object.tags,
}) })
end end
end end
function osm2pgsql.select_relation_members(relation)
if relation.tags.type == 'route' then
return { ways = osm2pgsql.way_member_ids(relation) }
end
end

View file

@ -11,6 +11,8 @@ RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, dist
speed, speed,
way_id::bigint as way_id way_id::bigint as way_id
FROM overtaking_event FROM overtaking_event
WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox; WHERE
zoom_level >= 10 AND
ST_Transform(overtaking_event.geometry, 3857) && bbox;
$$ LANGUAGE SQL IMMUTABLE; $$ LANGUAGE SQL IMMUTABLE;

View file

@ -0,0 +1,26 @@
DROP FUNCTION IF EXISTS layer_obs_regions(geometry, int);
CREATE OR REPLACE FUNCTION layer_obs_regions(bbox geometry, zoom_level int)
RETURNS TABLE(
region_id bigint,
geometry geometry,
name text,
admin_level int,
overtaking_event_count int
) AS $$
SELECT
region.relation_id::bigint as region_id,
ST_SimplifyPreserveTopology(region.geometry, ZRes(zoom_level + 2)) as geometry,
region.name as name,
region.admin_level as admin_level,
count(overtaking_event.id)::int as overtaking_event_count
FROM region
LEFT JOIN overtaking_event on ST_Within(ST_Transform(overtaking_event.geometry, 3857), region.geometry)
WHERE
zoom_level >= 4 AND
zoom_level <= 12 AND
ST_Transform(region.geometry, 3857) && bbox
GROUP BY region.relation_id, region.name, region.geometry, region.admin_level
$$ LANGUAGE SQL IMMUTABLE;

View file

@ -0,0 +1,23 @@
layer:
id: "obs_regions"
description: |
Statistics on administrative boundary areas ("regions")
buffer_size: 4
fields:
overtaking_event_count: |
Number of overtaking events.
name: |
Name of the region
admin_level: |
Administrative level of the boundary, as tagged in OpenStreetMap
defaults:
srs: EPSG:3785
datasource:
srid: 3857
geometry_field: geometry
key_field: region_id
key_field_as_attribute: no
query: (SELECT region_id, geometry, name, admin_level, overtaking_event_count FROM layer_obs_regions(!bbox!, z(!scale_denominator!))) AS t
schema:
- ./layer.sql

View file

@ -32,7 +32,9 @@ RETURNS TABLE(
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
zoom_level >= 10 AND
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;
$$ LANGUAGE SQL IMMUTABLE; $$ LANGUAGE SQL IMMUTABLE;

View file

@ -3,6 +3,7 @@ tileset:
layers: layers:
- layers/obs_events/obs_events.yaml - layers/obs_events/obs_events.yaml
- layers/obs_roads/obs_roads.yaml - layers/obs_roads/obs_roads.yaml
- layers/obs_regions/obs_regions.yaml
version: 0.6.1 version: 0.6.1
id: openbikesensor id: openbikesensor
description: > description: >