From f429ed32f34b97f82353971e15ec2d292df3ec7a Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Sun, 3 Apr 2022 16:06:34 +0200 Subject: [PATCH] Add administrative boundary import and display regional event count --- .../a049e5eb24dd_create_table_region.py | 36 +++++++++++++ api/obs/api/routes/frontend.py | 2 +- frontend/config.example.json | 2 +- frontend/src/components/ColorMapLegend.tsx | 4 +- frontend/src/mapstyles/index.js | 52 +++++++++++++++++++ frontend/src/pages/MapPage/LayerSidebar.tsx | 23 ++++++++ frontend/src/pages/MapPage/index.tsx | 13 +++-- frontend/src/reducers/mapConfig.ts | 6 +++ roads_import.lua | 29 +++++++++++ tile-generator/layers/obs_events/layer.sql | 4 +- tile-generator/layers/obs_regions/layer.sql | 26 ++++++++++ .../layers/obs_regions/obs_regions.yaml | 23 ++++++++ tile-generator/layers/obs_roads/layer.sql | 4 +- tile-generator/openbikesensor.yaml | 1 + 14 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 api/migrations/versions/a049e5eb24dd_create_table_region.py create mode 100644 tile-generator/layers/obs_regions/layer.sql create mode 100644 tile-generator/layers/obs_regions/obs_regions.yaml diff --git a/api/migrations/versions/a049e5eb24dd_create_table_region.py b/api/migrations/versions/a049e5eb24dd_create_table_region.py new file mode 100644 index 0000000..e5dcf7f --- /dev/null +++ b/api/migrations/versions/a049e5eb24dd_create_table_region.py @@ -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") diff --git a/api/obs/api/routes/frontend.py b/api/obs/api/routes/frontend.py index fb681a0..7ccaa70 100644 --- a/api/obs/api/routes/frontend.py +++ b/api/obs/api/routes/frontend.py @@ -26,7 +26,7 @@ if app.config.FRONTEND_CONFIG: .replace("111", "{x}") .replace("222", "{y}") ], - "minzoom": 12, + "minzoom": 0, "maxzoom": 14, } ), diff --git a/frontend/config.example.json b/frontend/config.example.json index 6566c6e..2918934 100644 --- a/frontend/config.example.json +++ b/frontend/config.example.json @@ -12,7 +12,7 @@ "obsMapSource": { "type": "vector", "tiles": ["https://portal.example.com/tiles/{z}/{x}/{y}.pbf"], - "minzoom": 12, + "minzoom": 0, "maxzoom": 14 } } diff --git a/frontend/src/components/ColorMapLegend.tsx b/frontend/src/components/ColorMapLegend.tsx index f0c4d48..ca09860 100644 --- a/frontend/src/components/ColorMapLegend.tsx +++ b/frontend/src/components/ColorMapLegend.tsx @@ -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 max = map[map.length - 1][0] const normalizeValue = (v) => (v - min) / (max - min) @@ -81,7 +81,7 @@ export default function ColorMapLegend({map, twoTicks = false}: {map: ColorMap, {tickValues.map(([value]) => ( - {value.toFixed(2)} + {value.toFixed(digits)} ))} diff --git a/frontend/src/mapstyles/index.js b/frontend/src/mapstyles/index.js index c3a62fb..77fb1a5 100644 --- a/frontend/src/mapstyles/index.js +++ b/frontend/src/mapstyles/index.js @@ -77,6 +77,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 => { // draft.paint['line-color'] = '#81D4FA' draft.paint['line-width'][4] = 1 diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index 9e38026..00e8fab 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -36,6 +36,7 @@ function LayerSidebar({ baseMap: {style}, obsRoads: {show: showRoads, showUntagged, attribute, maxCount}, obsEvents: {show: showEvents}, + obsRegions: {show: showRegions}, } = mapConfig return ( @@ -50,6 +51,28 @@ function LayerSidebar({ /> + + setMapConfigFlag('obsRegions.show', !showRegions)} + /> + + + {showRegions && ( + <> + Color regions based on event count + + + + + )} + } else { draft.filter = draft.filter[1] // remove '!' } + draft.minzoom = 10 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) : '#DDD' - draft.paint['line-opacity'][3] = 12 - draft.paint['line-opacity'][5] = 13 + // draft.paint['line-opacity'][3] = 12 + // draft.paint['line-opacity'][5] = 13 }) const getEventsLayer = () => ({ @@ -137,6 +139,9 @@ export default function MapPage() { layers.push(roadsLayer) } + const regionLayers = useMemo(() => getRegionLayers(), []) + layers.push(...regionLayers) + const eventsLayer = useMemo(() => getEventsLayer(), []) const eventsTextLayer = useMemo(() => getEventsTextLayer(), []) if (mapConfig.obsEvents.show) { diff --git a/frontend/src/reducers/mapConfig.ts b/frontend/src/reducers/mapConfig.ts index f0140cb..d4880f6 100644 --- a/frontend/src/reducers/mapConfig.ts +++ b/frontend/src/reducers/mapConfig.ts @@ -26,6 +26,9 @@ export type MapConfig = { obsEvents: { show: boolean; }; + obsRegions: { + show: boolean; + }; }; export const initialState: MapConfig = { @@ -41,6 +44,9 @@ export const initialState: MapConfig = { obsEvents: { show: false, }, + obsRegions: { + show: true, + }, }; type MapConfigAction = { diff --git a/roads_import.lua b/roads_import.lua index c314d50..5df9530 100644 --- a/roads_import.lua +++ b/roads_import.lua @@ -50,6 +50,9 @@ local MOTORWAY_TYPES = { "motorway_link", } +local ADMIN_LEVEL_MIN = 2 +local ADMIN_LEVEL_MAX = 8 + local ONEWAY_YES = {"yes", "true", "1"} local ONEWAY_REVERSE = {"reverse", "-1"} @@ -61,6 +64,14 @@ local roads = osm2pgsql.define_way_table('road', { { column = 'oneway', 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) if object.tags.highway and contains(HIGHWAY_TYPES, object.tags.highway) then local tags = object.tags @@ -112,3 +123,21 @@ function osm2pgsql.process_way(object) }) end 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 + +function osm2pgsql.select_relation_members(relation) + if relation.tags.type == 'route' then + return { ways = osm2pgsql.way_member_ids(relation) } + end +end diff --git a/tile-generator/layers/obs_events/layer.sql b/tile-generator/layers/obs_events/layer.sql index c57874a..03d085b 100644 --- a/tile-generator/layers/obs_events/layer.sql +++ b/tile-generator/layers/obs_events/layer.sql @@ -11,6 +11,8 @@ RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, dist speed, way_id::bigint as way_id 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; diff --git a/tile-generator/layers/obs_regions/layer.sql b/tile-generator/layers/obs_regions/layer.sql new file mode 100644 index 0000000..fda2a44 --- /dev/null +++ b/tile-generator/layers/obs_regions/layer.sql @@ -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; diff --git a/tile-generator/layers/obs_regions/obs_regions.yaml b/tile-generator/layers/obs_regions/obs_regions.yaml new file mode 100644 index 0000000..477bf45 --- /dev/null +++ b/tile-generator/layers/obs_regions/obs_regions.yaml @@ -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 diff --git a/tile-generator/layers/obs_roads/layer.sql b/tile-generator/layers/obs_roads/layer.sql index 92c4f2f..ce34d70 100644 --- a/tile-generator/layers/obs_roads/layer.sql +++ b/tile-generator/layers/obs_roads/layer.sql @@ -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) 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 + WHERE + zoom_level >= 10 AND + road.geometry && bbox GROUP BY road.name, road.way_id, road.geometry, road.directionality, r.dir, r.rev; $$ LANGUAGE SQL IMMUTABLE; diff --git a/tile-generator/openbikesensor.yaml b/tile-generator/openbikesensor.yaml index cf5fdd6..ecfc647 100644 --- a/tile-generator/openbikesensor.yaml +++ b/tile-generator/openbikesensor.yaml @@ -3,6 +3,7 @@ tileset: layers: - layers/obs_events/obs_events.yaml - layers/obs_roads/obs_roads.yaml + - layers/obs_regions/obs_regions.yaml version: 0.6.2 id: openbikesensor description: >