From ce8054b7aea6a1d5d4198a32408ec9c02f2e11e7 Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Fri, 31 Mar 2023 21:06:59 +0200 Subject: [PATCH] Use NUTS for region import, not OSM --- .../5c7755ead95d_remove_region_tags.py | 28 -- .../a049e5eb24dd_create_table_region.py | 5 +- .../b8b0fbae50a4_add_import_groups.py | 4 +- .../f4b0f460254d_add_osm_id_indexes.py | 4 +- api/obs/api/db.py | 2 +- api/obs/api/routes/stats.py | 7 +- api/requirements.txt | 1 + api/tools/import_osm.py | 55 +-- api/tools/import_regions.py | 93 +++++ api/tools/transform_osm.py | 27 +- frontend/src/App.tsx | 22 +- frontend/src/mapstyles/index.js | 296 ++++++++-------- frontend/src/pages/AcknowledgementsPage.tsx | 18 + frontend/src/pages/MapPage/LayerSidebar.tsx | 264 +++++++++----- frontend/src/pages/MapPage/index.tsx | 321 ++++++++++-------- frontend/src/pages/MapPage/styles.module.less | 7 + frontend/src/pages/index.js | 1 + frontend/src/translations/de.yaml | 44 +++ frontend/src/translations/en.yaml | 40 +++ frontend/src/translations/fr.yaml | 42 +++ tile-generator/layers/obs_events/layer.sql | 2 +- tile-generator/layers/obs_regions/layer.sql | 17 +- .../layers/obs_regions/obs_regions.yaml | 4 +- 23 files changed, 799 insertions(+), 505 deletions(-) delete mode 100644 api/migrations/versions/5c7755ead95d_remove_region_tags.py create mode 100755 api/tools/import_regions.py create mode 100644 frontend/src/pages/AcknowledgementsPage.tsx diff --git a/api/migrations/versions/5c7755ead95d_remove_region_tags.py b/api/migrations/versions/5c7755ead95d_remove_region_tags.py deleted file mode 100644 index 08fe361..0000000 --- a/api/migrations/versions/5c7755ead95d_remove_region_tags.py +++ /dev/null @@ -1,28 +0,0 @@ -"""remove region tags - -Revision ID: 5c7755ead95d -Revises: f7b21148126a -Create Date: 2023-03-26 09:36:46.808239 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import HSTORE - - -# revision identifiers, used by Alembic. -revision = "5c7755ead95d" -down_revision = "f7b21148126a" -branch_labels = None -depends_on = None - - -def upgrade(): - op.drop_column("region", "tags") - - -def downgrade(): - op.add_column( - "region", - sa.Column("tags", HSTORE, nullable=True), - ) diff --git a/api/migrations/versions/a049e5eb24dd_create_table_region.py b/api/migrations/versions/a049e5eb24dd_create_table_region.py index 89b1a72..aa434fc 100644 --- a/api/migrations/versions/a049e5eb24dd_create_table_region.py +++ b/api/migrations/versions/a049e5eb24dd_create_table_region.py @@ -21,13 +21,10 @@ depends_on = None def upgrade(): op.create_table( "region", - sa.Column( - "relation_id", sa.BIGINT, primary_key=True, index=True, autoincrement=False - ), + sa.Column("id", sa.String(24), primary_key=True, index=True), sa.Column("name", sa.Text), sa.Column("geometry", dbtype("GEOMETRY(GEOMETRY,3857)"), index=False), sa.Column("admin_level", sa.Integer, index=True), - sa.Column("tags", dbtype("HSTORE")), ) op.execute( "CREATE INDEX region_geometry_idx ON region USING GIST (geometry) WITH (FILLFACTOR=100);" diff --git a/api/migrations/versions/b8b0fbae50a4_add_import_groups.py b/api/migrations/versions/b8b0fbae50a4_add_import_groups.py index 044eeda..6a36c6b 100644 --- a/api/migrations/versions/b8b0fbae50a4_add_import_groups.py +++ b/api/migrations/versions/b8b0fbae50a4_add_import_groups.py @@ -1,7 +1,7 @@ """add import groups Revision ID: b8b0fbae50a4 -Revises: 5c7755ead95d +Revises: f7b21148126a Create Date: 2023-03-26 09:41:36.621203 """ @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "b8b0fbae50a4" -down_revision = "5c7755ead95d" +down_revision = "f7b21148126a" branch_labels = None depends_on = None diff --git a/api/migrations/versions/f4b0f460254d_add_osm_id_indexes.py b/api/migrations/versions/f4b0f460254d_add_osm_id_indexes.py index 2e3518c..b2b3663 100644 --- a/api/migrations/versions/f4b0f460254d_add_osm_id_indexes.py +++ b/api/migrations/versions/f4b0f460254d_add_osm_id_indexes.py @@ -18,9 +18,7 @@ depends_on = None def upgrade(): op.execute("CREATE INDEX IF NOT EXISTS ix_road_way_id ON road (way_id);") - op.execute( - "CREATE INDEX IF NOT EXISTS ix_region_relation_id ON region (relation_id);" - ) + op.execute("CREATE INDEX IF NOT EXISTS ix_region_id ON region (relation_id);") def downgrade(): diff --git a/api/obs/api/db.py b/api/obs/api/db.py index db36704..74d1f14 100644 --- a/api/obs/api/db.py +++ b/api/obs/api/db.py @@ -504,7 +504,7 @@ class Comment(Base): class Region(Base): __tablename__ = "region" - relation_id = Column(BIGINT, primary_key=True, index=True, autoincrement=False) + id = Column(String(24), primary_key=True, index=True) name = Column(Text) geometry = Column(GeometryGeometry) admin_level = Column(Integer, index=True) diff --git a/api/obs/api/routes/stats.py b/api/obs/api/routes/stats.py index 5875298..6d65a65 100644 --- a/api/obs/api/routes/stats.py +++ b/api/obs/api/routes/stats.py @@ -183,7 +183,7 @@ async def stats(req): query = ( select( [ - Region.relation_id.label("id"), + Region.id, Region.name, func.count(OvertakingEvent.id).label("overtaking_event_count"), ] @@ -195,12 +195,9 @@ async def stats(req): func.ST_Transform(OvertakingEvent.geometry, 3857), Region.geometry ), ) - .where(Region.admin_level == 6) .group_by( - Region.relation_id, + Region.id, Region.name, - Region.relation_id, - Region.admin_level, Region.geometry, ) .having(func.count(OvertakingEvent.id) > 0) diff --git a/api/requirements.txt b/api/requirements.txt index a26309f..6be07f1 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -17,3 +17,4 @@ osmium~=3.6.0 psycopg~=3.1.8 shapely~=2.0.1 pyproj~=3.4.1 +aiohttp~=3.8.1 diff --git a/api/tools/import_osm.py b/api/tools/import_osm.py index 539ec27..667ed87 100755 --- a/api/tools/import_osm.py +++ b/api/tools/import_osm.py @@ -18,17 +18,6 @@ log = logging.getLogger(__name__) ROAD_BUFFER = 1000 AREA_BUFFER = 100 -ROAD_TYPE = b"\x01" -REGION_TYPE = b"\x02" - - -@dataclass -class Region: - relation_id: int - name: str - admin_level: int - geometry: bytes - @dataclass class Road: @@ -40,12 +29,9 @@ class Road: geometry: bytes -data_types = {ROAD_TYPE: Road, REGION_TYPE: Region} - - -def read_file(filename, only_type: bytes): +def read_file(filename): """ - Reads a file iteratively, yielding Road and Region objects as they + Reads a file iteratively, yielding appear. Those may be mixed. """ @@ -53,11 +39,10 @@ def read_file(filename, only_type: bytes): unpacker = msgpack.Unpacker(f) try: while True: - type_id = unpacker.unpack() - data = unpacker.unpack() + type_id, *data = unpacker.unpack() - if type_id == only_type: - yield data_types[only_type](*data) + if type_id == b"\x01": + yield Road(*data) except msgpack.OutOfData: pass @@ -69,11 +54,8 @@ async def import_osm(connection, filename, import_group=None): # Pass 1: Find IDs only road_ids = [] - region_ids = [] - for item in read_file(filename, only_type=ROAD_TYPE): + for item in read_file(filename): road_ids.append(item.way_id) - for item in read_file(filename, only_type=REGION_TYPE): - region_ids.append(item.relation_id) async with connection.cursor() as cursor: log.info("Pass 1: Delete previously imported data") @@ -82,25 +64,17 @@ async def import_osm(connection, filename, import_group=None): await cursor.execute( "DELETE FROM road WHERE import_group = %s", (import_group,) ) - await cursor.execute( - "DELETE FROM region WHERE import_group = %s", (import_group,) - ) log.debug("Delete roads by way_id") for ids in chunk(road_ids, 10000): await cursor.execute("DELETE FROM road WHERE way_id = ANY(%s)", (ids,)) - log.debug("Delete regions by region_id") - for ids in chunk(region_ids, 10000): - await cursor.execute( - "DELETE FROM region WHERE relation_id = ANY(%s)", (ids,) - ) # Pass 2: Import log.info("Pass 2: Import roads") async with cursor.copy( "COPY road (way_id, name, zone, directionality, oneway, geometry, import_group) FROM STDIN" ) as copy: - for item in read_file(filename, ROAD_TYPE): + for item in read_file(filename): await copy.write_row( ( item.way_id, @@ -113,21 +87,6 @@ async def import_osm(connection, filename, import_group=None): ) ) - log.info(f"Pass 2: Import regions") - async with cursor.copy( - "COPY region (relation_id, name, geometry, admin_level, import_group) FROM STDIN" - ) as copy: - for item in read_file(filename, REGION_TYPE): - await copy.write_row( - ( - item.relation_id, - item.name, - bytes.hex(item.geometry), - item.admin_level, - import_group, - ) - ) - async def main(): logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") diff --git a/api/tools/import_regions.py b/api/tools/import_regions.py new file mode 100755 index 0000000..6d2d115 --- /dev/null +++ b/api/tools/import_regions.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +""" +This script downloads and/or imports regions for statistical analysis into the +PostGIS database. The regions are sourced from: + +* EU countries are covered by + [NUTS](https://ec.europa.eu/eurostat/web/gisco/geodata/reference-data/administrative-units-statistical-units/nuts). +""" + +import tempfile +from dataclasses import dataclass +import json +import asyncio +from os.path import basename, splitext +import sys +import logging +from typing import Optional + +import aiohttp +import psycopg + +from obs.api.app import app +from obs.api.utils import chunk + +log = logging.getLogger(__name__) + +NUTS_URL = "https://gisco-services.ec.europa.eu/distribution/v2/nuts/geojson/NUTS_RG_01M_2021_3857.geojson" + +from pyproj import Transformer + +project = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True).transform +from shapely.ops import transform +from shapely.geometry import shape +import shapely.wkb as wkb + + +async def import_nuts( + connection, filename=None, level: int = 3, import_group: Optional[str] = None +): + if import_group is None: + import_group = f"nuts{level}" + + if filename: + log.info("Load NUTS from file") + with open(filename) as f: + data = json.load(f) + else: + log.info("Download NUTS regions from europa.eu") + async with aiohttp.ClientSession() as session: + async with session.get(NUTS_URL) as resp: + data = await resp.json(content_type=None) + + async with connection.cursor() as cursor: + log.info( + "Delete previously imported regions with import group %s", import_group + ) + await cursor.execute( + "DELETE FROM region WHERE import_group = %s", (import_group,) + ) + + log.info("Import regions") + async with cursor.copy( + "COPY region (id, name, geometry, import_group) FROM STDIN" + ) as copy: + for feature in data["features"]: + if feature["properties"]["LEVL_CODE"] == level: + geometry = shape(feature["geometry"]) + # geometry = transform(project, geometry) + geometry = wkb.dumps(geometry) + geometry = bytes.hex(geometry) + await copy.write_row( + ( + feature["properties"]["NUTS_ID"], + feature["properties"]["NUTS_NAME"], + geometry, + import_group, + ) + ) + + +async def main(): + logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") + + url = app.config.POSTGRES_URL + url = url.replace("+asyncpg", "") + + async with await psycopg.AsyncConnection.connect(url) as connection: + await import_nuts(connection, sys.argv[1]) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/api/tools/transform_osm.py b/api/tools/transform_osm.py index d1634ff..cfccf40 100755 --- a/api/tools/transform_osm.py +++ b/api/tools/transform_osm.py @@ -170,33 +170,8 @@ class OSMHandler(osmium.SimpleHandler): geometry = wkb.loads(wkbfab.create_linestring(way), hex=True) geometry = transform(project, geometry) geometry = wkb.dumps(geometry) - self.packer.pack(b"\x01") - self.packer.pack([way.id, name, zone, directionality, oneway, geometry]) - - def area(self, area): - tags = area.tags - if tags.get("boundary") != "administrative": - return - - admin_level = parse_number(tags.get("admin_level")) - if not admin_level: - return - - if admin_level < ADMIN_LEVEL_MIN or admin_level > ADMIN_LEVEL_MAX: - return - - name = tags.get("name") - geometry = wkb.loads(wkbfab.create_multipolygon(area), hex=True) - geometry = transform(project, geometry) - geometry = wkb.dumps(geometry) - self.packer.pack(b"\x02") self.packer.pack( - [ - area.id, - name, - admin_level, - geometry, - ] + [b"\x01", way.id, name, zone, directionality, oneway, geometry] ) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96ad14f..5ba7c29 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import styles from "./App.module.less"; import { AVAILABLE_LOCALES, setLocale } from "i18n"; import { + AcknowledgementsPage, ExportPage, HomePage, LoginRedirectPage, @@ -186,6 +187,9 @@ const App = connect((state) => ({ login: state.login }))(function App({ + + + @@ -280,13 +284,17 @@ const App = connect((state) => ({ login: state.login }))(function App({ {t("App.footer.imprint")} - { config?.termsUrl && - - - {t('App.footer.terms')} - - - } + {config?.termsUrl && ( + + + {t("App.footer.terms")} + + + )} Math.round(v * 255))] + return ["rgb", ...arr.map((v) => Math.round(v * 255))]; } function rgbArrayToHtml(arr) { - return "#" + arr.map((v) => Math.round(v * 255).toString(16)).map(v => (v.length == 1 ? '0' : '') + v).join('') + return ( + "#" + + arr + .map((v) => Math.round(v * 255).toString(16)) + .map((v) => (v.length == 1 ? "0" : "") + v) + .join("") + ); } export function colormapToScale(colormap, value, min, max) { return [ - 'interpolate-hcl', - ['linear'], + "interpolate-hcl", + ["linear"], value, - ...colormap.flatMap((v, i, a) => [(i / (a.length - 1)) * (max - min) + min, v]), - ] + ...colormap.flatMap((v, i, a) => [ + (i / (a.length - 1)) * (max - min) + min, + v, + ]), + ]; } -export const viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20) -export const viridisSimpleHtml = simplifyColormap(viridisBase.map(rgbArrayToHtml), 10) -export const grayscale = ['#FFFFFF', '#000000'] -export const reds = [ - 'rgba( 255, 0, 0, 0)', - 'rgba( 255, 0, 0, 255)', -] +export const viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20); +export const viridisSimpleHtml = simplifyColormap( + viridisBase.map(rgbArrayToHtml), + 10 +); +export const grayscale = ["#FFFFFF", "#000000"]; +export const reds = ["rgba( 255, 0, 0, 0)", "rgba( 255, 0, 0, 255)"]; -export function colorByCount(attribute = 'event_count', maxCount, colormap = viridis) { - return colormapToScale(colormap, ['case', isValidAttribute(attribute), ['get', attribute], 0], 0, maxCount) +export function colorByCount( + attribute = "event_count", + maxCount, + colormap = viridis +) { + return colormapToScale( + colormap, + ["case", isValidAttribute(attribute), ["get", attribute], 0], + 0, + maxCount + ); } -var steps = {'rural': [1.6,1.8,2.0,2.2], - 'urban': [1.1,1.3,1.5,1.7]} +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']]] + if (attribute.endsWith("zone")) { + return ["in", ["get", attribute], ["literal", ["rural", "urban"]]]; } - return ['to-boolean', ['get', attribute]] + return ["to-boolean", ["get", attribute]]; } export function borderByZone() { - return ["match", ['get', 'zone'], - "rural", "cyan", - "urban", "blue", - "purple" - ] + return ["match", ["get", "zone"], "rural", "cyan", "urban", "blue", "purple"]; } -export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC', zone='urban') { - +export function colorByDistance( + attribute = "distance_overtaker_mean", + fallback = "#ABC", + zone = "urban" +) { return [ - 'case', - ['!', isValidAttribute(attribute)], + "case", + ["!", isValidAttribute(attribute)], fallback, - ["match", ['get', 'zone'], "rural", [ - 'step', - ['get', attribute], - 'rgba(150, 0, 0, 1)', - steps['rural'][0], - 'rgba(255, 0, 0, 1)', - steps['rural'][1], - 'rgba(255, 220, 0, 1)', - steps['rural'][2], - 'rgba(67, 200, 0, 1)', - 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)', + "match", + ["get", "zone"], + "rural", + [ + "step", + ["get", attribute], + "rgba(150, 0, 0, 1)", + steps["rural"][0], + "rgba(255, 0, 0, 1)", + steps["rural"][1], + "rgba(255, 220, 0, 1)", + steps["rural"][2], + "rgba(67, 200, 0, 1)", + 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)", + ], ], - [ - '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)', - ] - ] - ] + ]; } export const trackLayer = { - type: 'line', + type: "line", paint: { - 'line-width': ['interpolate', ['linear'], ['zoom'], 14, 2, 17, 5], - 'line-color': '#F06292', - 'line-opacity': 0.6, + "line-width": ["interpolate", ["linear"], ["zoom"], 14, 2, 17, 5], + "line-color": "#F06292", + "line-opacity": 0.6, }, -} +}; -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], - [">", "overtaking_event_count", 0], - ], - "paint": { - "fill-color": baseColor, - "fill-antialias": true, - "fill-opacity": [ - "interpolate", - ["linear"], - [ - "log10", - [ - "get", - "overtaking_event_count" - ] +export const getRegionLayers = ( + adminLevel = 6, + baseColor = "#00897B", + maxValue = 5000 +) => [ + { + id: "region", + type: "fill", + source: "obs", + "source-layer": "obs_regions", + minzoom: 0, + maxzoom: 10, + // filter: [">", "overtaking_event_count", 0], + paint: { + "fill-color": baseColor, + "fill-antialias": true, + "fill-opacity": [ + "interpolate", + ["linear"], + ["log10", ["get", "overtaking_event_count"]], + 0, + 0, + Math.log10(maxValue), + 0.9, ], - 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], - [">", "overtaking_event_count", 0], - ], - "paint": { - "line-width": 1, - "line-color": baseColor, + { + id: "region-border", + type: "line", + source: "obs", + "source-layer": "obs_regions", + minzoom: 0, + maxzoom: 10, + // filter: [">", "overtaking_event_count", 0], + paint: { + "line-width": 1, + "line-color": baseColor, + }, + layout: { + "line-join": "round", + "line-cap": "round", + }, }, - "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-width'][4] = 1 - draft.paint['line-width'][6] = 2 - draft.paint['line-dasharray'] = [3, 3] - delete draft.paint['line-opacity'] -}) + draft.paint["line-width"][4] = 1; + draft.paint["line-width"][6] = 2; + draft.paint["line-dasharray"] = [3, 3]; + delete draft.paint["line-opacity"]; +}); -export const basemap = positron +export const basemap = positron; diff --git a/frontend/src/pages/AcknowledgementsPage.tsx b/frontend/src/pages/AcknowledgementsPage.tsx new file mode 100644 index 0000000..cc9cb19 --- /dev/null +++ b/frontend/src/pages/AcknowledgementsPage.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Header } from "semantic-ui-react"; +import { useTranslation } from "react-i18next"; +import Markdown from "react-markdown"; + +import { Page } from "components"; + +export default function AcknowledgementsPage() { + const { t } = useTranslation(); + const title = t("AcknowledgementsPage.title"); + + return ( + +
{title}
+ {t("AcknowledgementsPage.information")} +
+ ); +} diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index 7dcf698..00286d2 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -1,56 +1,81 @@ -import React from 'react' -import _ from 'lodash' -import {connect} from 'react-redux' -import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react' -import {useTranslation} from 'react-i18next' +import React from "react"; +import _ from "lodash"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { + List, + Select, + Input, + Divider, + Label, + Checkbox, + Header, +} from "semantic-ui-react"; +import { useTranslation } from "react-i18next"; import { MapConfig, setMapConfigFlag as setMapConfigFlagAction, initialState as defaultMapConfig, -} from 'reducers/mapConfig' -import {colorByDistance, colorByCount, viridisSimpleHtml} from 'mapstyles' -import {ColorMapLegend, DiscreteColorMapLegend} from 'components' +} from "reducers/mapConfig"; +import { colorByDistance, colorByCount, viridisSimpleHtml } from "mapstyles"; +import { ColorMapLegend, DiscreteColorMapLegend } from "components"; +import styles from "./styles.module.less"; -const BASEMAP_STYLE_OPTIONS = ['positron', 'bright'] +const BASEMAP_STYLE_OPTIONS = ["positron", "bright"]; const ROAD_ATTRIBUTE_OPTIONS = [ - 'distance_overtaker_mean', - 'distance_overtaker_min', - 'distance_overtaker_max', - 'distance_overtaker_median', - 'overtaking_event_count', - 'usage_count', - 'zone', -] + "distance_overtaker_mean", + "distance_overtaker_min", + "distance_overtaker_max", + "distance_overtaker_median", + "overtaking_event_count", + "usage_count", + "zone", +]; -const DATE_FILTER_MODES = ['none', 'range', 'threshold'] +const DATE_FILTER_MODES = ["none", "range", "threshold"]; -type User = Object +type User = Object; function LayerSidebar({ mapConfig, login, setMapConfigFlag, }: { - login: User | null - mapConfig: MapConfig - setMapConfigFlag: (flag: string, value: unknown) => void + login: User | null; + mapConfig: MapConfig; + setMapConfigFlag: (flag: string, value: unknown) => void; }) { - const {t} = useTranslation() + const { t } = useTranslation(); const { - baseMap: {style}, - obsRoads: {show: showRoads, showUntagged, attribute, maxCount}, - obsEvents: {show: showEvents}, - obsRegions: {show: showRegions}, - filters: {currentUser: filtersCurrentUser, dateMode, startDate, endDate, thresholdAfter}, - } = mapConfig + baseMap: { style }, + obsRoads: { show: showRoads, showUntagged, attribute, maxCount }, + obsEvents: { show: showEvents }, + obsRegions: { show: showRegions }, + filters: { + currentUser: filtersCurrentUser, + dateMode, + startDate, + endDate, + thresholdAfter, + }, + } = mapConfig; + + const openStreetMapCopyright = ( + + {t("MapPage.sidebar.copyright.openStreetMap")}{" "} + + {t("MapPage.sidebar.copyright.learnMore")} + + + ); return (
- {t('MapPage.sidebar.baseMap.style.label')} + {t("MapPage.sidebar.baseMap.style.label")} ({ @@ -123,53 +163,78 @@ function LayerSidebar({ text: t(`MapPage.sidebar.obsRoads.attribute.${value}`), }))} value={attribute} - onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)} + onChange={(_e, { value }) => + setMapConfigFlag("obsRoads.attribute", value) + } /> - {attribute.endsWith('_count') ? ( + {attribute.endsWith("_count") ? ( <> - {t('MapPage.sidebar.obsRoads.maxCount.label')} + + {t("MapPage.sidebar.obsRoads.maxCount.label")} + setMapConfigFlag('obsRoads.maxCount', value)} + onChange={(_e, { value }) => + setMapConfigFlag("obsRoads.maxCount", value) + } /> - ) : attribute.endsWith('zone') ? ( + ) : attribute.endsWith("zone") ? ( <> - ) : ( <> - {_.upperFirst(t('general.zone.urban'))} - + + {_.upperFirst(t("general.zone.urban"))} + + - {_.upperFirst(t('general.zone.rural'))} - + + {_.upperFirst(t("general.zone.rural"))} + + )} + {openStreetMapCopyright} )} @@ -178,36 +243,40 @@ function LayerSidebar({ toggle size="small" id="obsEvents.show" - style={{float: 'right'}} + style={{ float: "right" }} checked={showEvents} - onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)} + onChange={() => setMapConfigFlag("obsEvents.show", !showEvents)} /> {showEvents && ( <> - {_.upperFirst(t('general.zone.urban'))} - + {_.upperFirst(t("general.zone.urban"))} + - {_.upperFirst(t('general.zone.rural'))} - + {_.upperFirst(t("general.zone.rural"))} + )} -
{t('MapPage.sidebar.filters.title')}
+
{t("MapPage.sidebar.filters.title")}
{login && ( <> -
{t('MapPage.sidebar.filters.userData')}
+
{t("MapPage.sidebar.filters.userData")}
@@ -216,13 +285,15 @@ function LayerSidebar({ size="small" id="filters.currentUser" checked={filtersCurrentUser} - onChange={() => setMapConfigFlag('filters.currentUser', !filtersCurrentUser)} - label={t('MapPage.sidebar.filters.currentUser')} + onChange={() => + setMapConfigFlag("filters.currentUser", !filtersCurrentUser) + } + label={t("MapPage.sidebar.filters.currentUser")} /> -
{t('MapPage.sidebar.filters.dateRange')}
+
{t("MapPage.sidebar.filters.dateRange")}
@@ -233,12 +304,14 @@ function LayerSidebar({ key: value, text: t(`MapPage.sidebar.filters.dateMode.${value}`), }))} - value={dateMode ?? 'none'} - onChange={(_e, {value}) => setMapConfigFlag('filters.dateMode', value)} + value={dateMode ?? "none"} + onChange={(_e, { value }) => + setMapConfigFlag("filters.dateMode", value) + } /> - {dateMode == 'range' && ( + {dateMode == "range" && ( setMapConfigFlag('filters.startDate', value)} + onChange={(_e, { value }) => + setMapConfigFlag("filters.startDate", value) + } value={startDate ?? null} - label={t('MapPage.sidebar.filters.start')} + label={t("MapPage.sidebar.filters.start")} /> )} - {dateMode == 'range' && ( + {dateMode == "range" && ( setMapConfigFlag('filters.endDate', value)} + onChange={(_e, { value }) => + setMapConfigFlag("filters.endDate", value) + } value={endDate ?? null} - label={t('MapPage.sidebar.filters.end')} + label={t("MapPage.sidebar.filters.end")} /> )} - {dateMode == 'threshold' && ( + {dateMode == "threshold" && ( setMapConfigFlag('filters.startDate', value)} - label={t('MapPage.sidebar.filters.threshold')} + onChange={(_e, { value }) => + setMapConfigFlag("filters.startDate", value) + } + label={t("MapPage.sidebar.filters.threshold")} /> )} - {dateMode == 'threshold' && ( + {dateMode == "threshold" && ( - {t('MapPage.sidebar.filters.before')}{' '} + {t("MapPage.sidebar.filters.before")}{" "} setMapConfigFlag('filters.thresholdAfter', !thresholdAfter)} + onChange={() => + setMapConfigFlag( + "filters.thresholdAfter", + !thresholdAfter + ) + } id="filters.thresholdAfter" - />{' '} - {t('MapPage.sidebar.filters.after')} + />{" "} + {t("MapPage.sidebar.filters.after")} )} )} - {!login && {t('MapPage.sidebar.filters.needsLogin')}} + {!login && ( + {t("MapPage.sidebar.filters.needsLogin")} + )}
- ) + ); } export default connect( @@ -316,6 +402,6 @@ export default connect( ), login: state.login, }), - {setMapConfigFlag: setMapConfigFlagAction} + { setMapConfigFlag: setMapConfigFlagAction } // -)(LayerSidebar) +)(LayerSidebar); diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx index 0af4db2..a5a5ac7 100644 --- a/frontend/src/pages/MapPage/index.tsx +++ b/frontend/src/pages/MapPage/index.tsx @@ -1,253 +1,296 @@ -import React, {useState, useCallback, useMemo, useRef} from 'react' -import _ from 'lodash' -import {connect} from 'react-redux' -import {Button} from 'semantic-ui-react' -import {Layer, Source} from 'react-map-gl' -import produce from 'immer' -import classNames from 'classnames' +import React, { useState, useCallback, useMemo, useRef } from "react"; +import _ from "lodash"; +import { connect } from "react-redux"; +import { Button } from "semantic-ui-react"; +import { Layer, Source } from "react-map-gl"; +import produce from "immer"; +import classNames from "classnames"; -import api from 'api' -import type {Location} from 'types' -import {Page, Map} from 'components' -import {useConfig} from 'config' -import {colorByDistance, colorByCount, getRegionLayers, borderByZone, isValidAttribute} from 'mapstyles' -import {useMapConfig} from 'reducers/mapConfig' +import api from "api"; +import type { Location } from "types"; +import { Page, Map } from "components"; +import { useConfig } from "config"; +import { + colorByDistance, + colorByCount, + getRegionLayers, + borderByZone, + isValidAttribute, +} from "mapstyles"; +import { useMapConfig } from "reducers/mapConfig"; -import RoadInfo, {RoadInfoType} from './RoadInfo' -import RegionInfo from './RegionInfo' -import LayerSidebar from './LayerSidebar' -import styles from './styles.module.less' +import RoadInfo, { RoadInfoType } from "./RoadInfo"; +import RegionInfo from "./RegionInfo"; +import LayerSidebar from "./LayerSidebar"; +import styles from "./styles.module.less"; const untaggedRoadsLayer = { - id: 'obs_roads_untagged', - type: 'line', - source: 'obs', - 'source-layer': 'obs_roads', + id: "obs_roads_untagged", + type: "line", + source: "obs", + "source-layer": "obs_roads", minzoom: 12, - filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], + filter: ["!", ["to-boolean", ["get", "distance_overtaker_mean"]]], layout: { - 'line-cap': 'round', - 'line-join': 'round', + "line-cap": "round", + "line-join": "round", }, paint: { - 'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2], - 'line-color': '#ABC', + "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 2, 17, 2], + "line-color": "#ABC", // "line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1], - 'line-offset': [ - 'interpolate', - ['exponential', 1.5], - ['zoom'], + "line-offset": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], 12, - ['get', 'offset_direction'], + ["get", "offset_direction"], 19, - ['*', ['get', 'offset_direction'], 8], + ["*", ["get", "offset_direction"], 8], ], }, -} +}; const getUntaggedRoadsLayer = (colorAttribute) => produce(untaggedRoadsLayer, (draft) => { - draft.filter = ['!', isValidAttribute(colorAttribute)] - }) + draft.filter = ["!", isValidAttribute(colorAttribute)]; + }); const getRoadsLayer = (colorAttribute, maxCount) => produce(untaggedRoadsLayer, (draft) => { - draft.id = 'obs_roads_normal' - draft.filter = isValidAttribute(colorAttribute) - draft.minzoom = 10 - draft.paint['line-width'][6] = 6 // scale bigger on zoom - draft.paint['line-color'] = colorAttribute.startsWith('distance_') + draft.id = "obs_roads_normal"; + draft.filter = isValidAttribute(colorAttribute); + 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') + : colorAttribute.endsWith("_count") ? colorByCount(colorAttribute, maxCount) - : colorAttribute.endsWith('zone') + : colorAttribute.endsWith("zone") ? borderByZone() - : '#DDD' + : "#DDD"; // draft.paint["line-opacity"][3] = 12; // draft.paint["line-opacity"][5] = 13; - }) + }); const getEventsLayer = () => ({ - id: 'obs_events', - type: 'circle', - source: 'obs', - 'source-layer': 'obs_events', + id: "obs_events", + type: "circle", + source: "obs", + "source-layer": "obs_events", paint: { - 'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8], - 'circle-color': colorByDistance('distance_overtaker'), + "circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 3, 17, 8], + "circle-color": colorByDistance("distance_overtaker"), }, - minzoom: 11, -}) + minzoom: 8, +}); const getEventsTextLayer = () => ({ - id: 'obs_events_text', - type: 'symbol', + id: "obs_events_text", + type: "symbol", minzoom: 18, - source: 'obs', - 'source-layer': 'obs_events', + source: "obs", + "source-layer": "obs_events", layout: { - 'text-field': [ - 'number-format', - ['get', 'distance_overtaker'], - {'min-fraction-digits': 2, 'max-fraction-digits': 2}, + "text-field": [ + "number-format", + ["get", "distance_overtaker"], + { "min-fraction-digits": 2, "max-fraction-digits": 2 }, ], - 'text-allow-overlap': true, - 'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'], - 'text-size': 14, - 'text-keep-upright': false, - 'text-anchor': 'left', - 'text-radial-offset': 1, - 'text-rotate': ['-', 90, ['*', ['get', 'course'], 180 / Math.PI]], - 'text-rotation-alignment': 'map', + "text-allow-overlap": true, + "text-font": ["Open Sans Bold", "Arial Unicode MS Regular"], + "text-size": 14, + "text-keep-upright": false, + "text-anchor": "left", + "text-radial-offset": 1, + "text-rotate": ["-", 90, ["*", ["get", "course"], 180 / Math.PI]], + "text-rotation-alignment": "map", }, paint: { - 'text-halo-color': 'rgba(255, 255, 255, 1)', - 'text-halo-width': 1, - 'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1], + "text-halo-color": "rgba(255, 255, 255, 1)", + "text-halo-width": 1, + "text-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.3, 1], }, -}) +}); interface RegionInfo { properties: { - admin_level: number - name: string - overtaking_event_count: number - } + admin_level: number; + name: string; + overtaking_event_count: number; + }; } -type Details = {type: 'road'; road: RoadInfoType} | {type: 'region'; region: RegionInfo} +type Details = + | { type: "road"; road: RoadInfoType } + | { type: "region"; region: RegionInfo }; -function MapPage({login}) { - const {obsMapSource, banner} = useConfig() || {} - const [details, setDetails] = useState(null) +function MapPage({ login }) { + const { obsMapSource, banner } = useConfig() || {}; + const [details, setDetails] = useState(null); - const onCloseDetails = useCallback(() => setDetails(null), [setDetails]) + const onCloseDetails = useCallback(() => setDetails(null), [setDetails]); - const mapConfig = useMapConfig() + const mapConfig = useMapConfig(); - const viewportRef = useRef() - const mapInfoPortal = useRef() + const viewportRef = useRef(); + const mapInfoPortal = useRef(); const onViewportChange = useCallback( (viewport) => { - viewportRef.current = viewport + viewportRef.current = viewport; }, [viewportRef] - ) + ); const onClick = useCallback( async (e) => { // check if we clicked inside the mapInfoBox, if so, early exit - let node = e.target + let node = e.target; while (node) { - if ([styles.mapInfoBox, styles.mapToolbar].some((className) => node?.classList?.contains(className))) { - return + if ( + [styles.mapInfoBox, styles.mapToolbar].some((className) => + node?.classList?.contains(className) + ) + ) { + return; } - node = node.parentNode + node = node.parentNode; } - const {zoom} = viewportRef.current + const { zoom } = viewportRef.current; if (zoom < 10) { - const clickedRegion = e.features?.find((f) => f.source === 'obs' && f.sourceLayer === 'obs_regions') - setDetails(clickedRegion ? {type: 'region', region: clickedRegion} : null) + const clickedRegion = e.features?.find( + (f) => f.source === "obs" && f.sourceLayer === "obs_regions" + ); + setDetails( + clickedRegion ? { type: "region", region: clickedRegion } : null + ); } else { - const road = await api.get('/mapdetails/road', { + const road = await api.get("/mapdetails/road", { query: { longitude: e.lngLat[0], latitude: e.lngLat[1], radius: 100, }, - }) - setDetails(road?.road ? {type: 'road', road} : null) + }); + setDetails(road?.road ? { type: "road", road } : null); } }, [setDetails] - ) + ); - const [layerSidebar, setLayerSidebar] = useState(true) + const [layerSidebar, setLayerSidebar] = useState(true); const { - obsRoads: {attribute, maxCount}, - } = mapConfig + obsRoads: { attribute, maxCount }, + } = mapConfig; - const layers = [] + const layers = []; - const untaggedRoadsLayerCustom = useMemo(() => getUntaggedRoadsLayer(attribute), [attribute]) + const untaggedRoadsLayerCustom = useMemo( + () => getUntaggedRoadsLayer(attribute), + [attribute] + ); if (mapConfig.obsRoads.show && mapConfig.obsRoads.showUntagged) { - layers.push(untaggedRoadsLayerCustom) + layers.push(untaggedRoadsLayerCustom); } - const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount]) + const roadsLayer = useMemo( + () => getRoadsLayer(attribute, maxCount), + [attribute, maxCount] + ); if (mapConfig.obsRoads.show) { - layers.push(roadsLayer) + layers.push(roadsLayer); } - const regionLayers = useMemo(() => getRegionLayers(), []) + const regionLayers = useMemo(() => getRegionLayers(), []); if (mapConfig.obsRegions.show) { - layers.push(...regionLayers) + layers.push(...regionLayers); } - const eventsLayer = useMemo(() => getEventsLayer(), []) - const eventsTextLayer = useMemo(() => getEventsTextLayer(), []) + const eventsLayer = useMemo(() => getEventsLayer(), []); + const eventsTextLayer = useMemo(() => getEventsTextLayer(), []); if (mapConfig.obsEvents.show) { - layers.push(eventsLayer) - layers.push(eventsTextLayer) + layers.push(eventsLayer); + layers.push(eventsTextLayer); } const onToggleLayerSidebarButtonClick = useCallback( (e) => { - e.stopPropagation() - e.preventDefault() - console.log('toggl;e') - setLayerSidebar((v) => !v) + e.stopPropagation(); + e.preventDefault(); + console.log("toggl;e"); + setLayerSidebar((v) => !v); }, [setLayerSidebar] - ) + ); if (!obsMapSource) { - return null + return null; } const tiles = obsMapSource?.tiles?.map((tileUrl: string) => { - const query = new URLSearchParams() + const query = new URLSearchParams(); if (login) { if (mapConfig.filters.currentUser) { - query.append('user', login.id) + query.append("user", login.id); } - if (mapConfig.filters.dateMode === 'range') { + if (mapConfig.filters.dateMode === "range") { if (mapConfig.filters.startDate) { - query.append('start', mapConfig.filters.startDate) + query.append("start", mapConfig.filters.startDate); } if (mapConfig.filters.endDate) { - query.append('end', mapConfig.filters.endDate) + query.append("end", mapConfig.filters.endDate); } - } else if (mapConfig.filters.dateMode === 'threshold') { + } else if (mapConfig.filters.dateMode === "threshold") { if (mapConfig.filters.startDate) { - query.append(mapConfig.filters.thresholdAfter ? 'start' : 'end', mapConfig.filters.startDate) + query.append( + mapConfig.filters.thresholdAfter ? "start" : "end", + mapConfig.filters.startDate + ); } } } - const queryString = String(query) - return tileUrl + (queryString ? '?' : '') + queryString - }) + const queryString = String(query); + return tileUrl + (queryString ? "?" : "") + queryString; + }); - const hasFilters: boolean = login && (mapConfig.filters.currentUser || mapConfig.filters.dateMode !== 'none') + const hasFilters: boolean = + login && + (mapConfig.filters.currentUser || mapConfig.filters.dateMode !== "none"); return ( -
+
{layerSidebar && (
)}
- +
-
{layers.map((layer) => ( @@ -255,23 +298,27 @@ function MapPage({login}) { ))} - {details?.type === 'road' && details?.road?.road && ( + {details?.type === "road" && details?.road?.road && ( )} - {details?.type === 'region' && details?.region && ( - + {details?.type === "region" && details?.region && ( + )}
- ) + ); } -export default connect((state) => ({login: state.login}))(MapPage) +export default connect((state) => ({ login: state.login }))(MapPage); diff --git a/frontend/src/pages/MapPage/styles.module.less b/frontend/src/pages/MapPage/styles.module.less index 6d18337..8519d84 100644 --- a/frontend/src/pages/MapPage/styles.module.less +++ b/frontend/src/pages/MapPage/styles.module.less @@ -32,6 +32,13 @@ padding: 16px; } +.copyright { + color: #888; + font-size: 0.8em; + line-height: 1.4; + margin-block-start: 1em; +} + .mapToolbar { position: absolute; left: 16px; diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js index 6d0a958..6cab239 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.js @@ -1,3 +1,4 @@ +export { default as AcknowledgementsPage } from "./AcknowledgementsPage"; export { default as ExportPage } from "./ExportPage"; export { default as HomePage } from "./HomePage"; export { default as LoginRedirectPage } from "./LoginRedirectPage"; diff --git a/frontend/src/translations/de.yaml b/frontend/src/translations/de.yaml index b46a7ae..aa0f57c 100644 --- a/frontend/src/translations/de.yaml +++ b/frontend/src/translations/de.yaml @@ -134,6 +134,11 @@ NotFoundPage: MapPage: sidebar: + copyright: + learnMore: Mehr erfahren + openStreetMap: © OpenStreetMap + boundaries: © EuroGeographics bezüglich der Verwaltungsgrenzen + baseMap: style: label: Stil der Basiskarte @@ -365,3 +370,42 @@ RegionStats: title: Top-Regionen regionName: Region eventCount: Anzahl Überholungen + +AcknowledgementsPage: + title: Danksagungen + information: | + Diese Software kann nur funktionieren Dank der Arbeit vieler anderer + Menschen. Auf dieser Seite möchten wir die genutzten Datenbanken und + Bibliotheken hervorheben. + + Wenn du die von dieser Software angezeigten Daten nutzen möchtest, zum + Beispiel Exporte, Downloads, Screenshots oder andere Extrakte, kann es sein + dass du einen oder mehrere dieser Datensätze weiterverwendest. Das + entsprechende Urheberrechts findet dann Anwendung, prüfe also bitte + sorgfältig die Lizenzbedigungen und ob sie für dich relevant sind. Denke + daran, die Autor:innen der Quellen angemessen zu attribuieren, auf deren + Arbeit dein Werk direkt oder indirekt durch Verwendung der Daten aufbaut. + + ## Basiskarte + + Die Basiskarte wird durch Daten aus der + [OpenStreetMap](openstreetmap.org/copyright) erzeugt und verwendet das + Schema und die Stile von [OpenMapTiles](https://openmaptiles.org/). + + ## Straßenzüge + + Informationen über Straßenzüge werden für die Verarbeitung hochgeladener + Fahrten und für die Anzeige der Straßensegmente auf der Karte verwendet. + Diese Informationen stammen aus der + [OpenStreetMap](openstreetmap.org/copyright). + + ## Verwaltungsgrenzen + + Verwaltungsgrenzen werden für statistische Auswertung der Regionen und + Anzeige auf der Karte verwendet und werden von + [NUTS](https://ec.europa.eu/eurostat/web/gisco/geodata/reference-data/administrative-units-statistical-units) + importiert. Es gelten gesonderte Bedingungen für die Nutzung dieser Daten, + bitte folge dem obigen Link um mehr zu erfahren, wenn du diese Daten in + eigenen Werken nutzen möchtest. + + © EuroGeographics bezüglich der Verwaltungsgrenzen diff --git a/frontend/src/translations/en.yaml b/frontend/src/translations/en.yaml index 34c7408..40adbd7 100644 --- a/frontend/src/translations/en.yaml +++ b/frontend/src/translations/en.yaml @@ -140,6 +140,11 @@ NotFoundPage: MapPage: sidebar: + copyright: + learnMore: Learn more + openStreetMap: © OpenStreetMap + boundaries: © EuroGeographics for the administrative boundaries + baseMap: style: label: Basemap Style @@ -363,3 +368,38 @@ RegionStats: title: Top regions regionName: Region name eventCount: Event count + +AcknowledgementsPage: + title: Acknowledgements + information: | + This software is only able to function thanks to the work of other people. + On this page we'd like to acknowledge the work we depend on and the + databases and libraries we use. + + If you use any data provided by this software, including exports, + downloads, screenshots or other extracted information, you might be making + use of these datasets, and their copyright provisision might apply to you. + Please take care to review this on a case by case basis and attribute the + origins of the data you are using in your derivate work, whether that is + directly or indirectly through this software. + + ## Basemap + + Basemap data is generally generated from + [OpenStreetMap](openstreetmap.org/copyright) data and is using the + [OpenMapTiles](https://openmaptiles.org/) schema and styles. + + ## Roadway information + + Roadway information is used to process uploaded tracks and to display road + segment statistics. This data is extracted from the + [OpenStreetMap](openstreetmap.org/copyright). + + ## Region boundaries + + Region boundaries for statistical analysis and map display are imported from + [NUTS](https://ec.europa.eu/eurostat/web/gisco/geodata/reference-data/administrative-units-statistical-units). + Provisions apply to the use of region boundary data, please follow above + link to learn more if you want to use this information in your derivative work. + + © EuroGeographics for the administrative boundaries diff --git a/frontend/src/translations/fr.yaml b/frontend/src/translations/fr.yaml index 726f2f1..c14bad5 100644 --- a/frontend/src/translations/fr.yaml +++ b/frontend/src/translations/fr.yaml @@ -140,6 +140,11 @@ NotFoundPage: MapPage: sidebar: + copyright: + learnMore: En savoir plus + openStreetMap: © OpenStreetMap + boundaries: © EuroGeographics pour les limites administratives + baseMap: style: label: Style de fond de carte @@ -361,3 +366,40 @@ RegionStats: title: Top régions regionName: Nom de la région eventCount: Nombre de dépassements + +AcknowledgementsPage: + title: Remerciements + information: | + Ce logiciel ne peut fonctionner que grâce au travail d'autres personnes. + Sur cette page, nous aimerions reconnaître le travail dont nous dépendons et le + bases de données et bibliothèques que nous utilisons. + + Si vous utilisez des données fournies par ce logiciel, y compris les exportations, + téléchargements, captures d'écran ou autres informations extraites, vous pourriez faire + l'utilisation de ces ensembles de données, et leur disposition sur le droit d'auteur peut s'appliquer à vous. + Veuillez prendre soin d'examiner cela au cas par cas et d'attribuer le + origines des données que vous utilisez dans votre travail dérivé, que ce soit + directement ou indirectement via ce logiciel. + + ## Fond de carte + + Les données de fond de carte sont généralement générées à partir de + [OpenStreetMap](openstreetmap.org/copyright) données et utilise le + [OpenMapTiles](https://openmaptiles.org/) schéma et styles. + + ## Informations routières + + Les informations routières sont utilisées pour traiter les pistes + téléchargées et pour afficher la route statistiques de segments. Ces + données sont extraites du [OpenStreetMap](openstreetmap.org/copyright). + + ## Limites administratives + + Les limites administratives pour l'analyse statistique et l'affichage de la + carte sont importées de + [NUTS](https://ec.europa.eu/eurostat/web/gisco/geodata/reference-data/administrative-units-statistical-units). + Des dispositions s'appliquent à l'utilisation des données sur les limites + de la région, veuillez suivre ci-dessus lien pour en savoir plus si vous + souhaitez utiliser ces informations dans votre travail dérivé. + + © EuroGeographics pour les limites administratives diff --git a/tile-generator/layers/obs_events/layer.sql b/tile-generator/layers/obs_events/layer.sql index ffac876..b4448a0 100644 --- a/tile-generator/layers/obs_events/layer.sql +++ b/tile-generator/layers/obs_events/layer.sql @@ -15,7 +15,7 @@ RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, dist FULL OUTER JOIN road ON road.way_id = overtaking_event.way_id JOIN track on track.id = overtaking_event.track_id WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox - AND zoom_level >= 10 + AND zoom_level >= 8 AND (user_id is NULL OR user_id = track.author_id) AND time BETWEEN COALESCE(min_time, '1900-01-01'::timestamp) AND COALESCE(max_time, '2100-01-01'::timestamp); diff --git a/tile-generator/layers/obs_regions/layer.sql b/tile-generator/layers/obs_regions/layer.sql index 8bfca6b..3581b69 100644 --- a/tile-generator/layers/obs_regions/layer.sql +++ b/tile-generator/layers/obs_regions/layer.sql @@ -2,26 +2,25 @@ 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, + region_id int, 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.id as region_id, + NULL::int as region_id, + -- ST_SimplifyPreserveTopology(region.geometry, ZRes(zoom_level + 2)) as geometry, + region.geometry 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) + LEFT OUTER JOIN overtaking_event on ST_Within(ST_Transform(overtaking_event.geometry, 3857), region.geometry) WHERE - zoom_level >= 4 AND + zoom_level >= 3 AND zoom_level <= 12 AND - region.admin_level = 6 AND region.geometry && bbox - GROUP BY region.relation_id, region.name, region.geometry, region.admin_level + GROUP BY region.id, region.name, region.geometry $$ LANGUAGE SQL IMMUTABLE; diff --git a/tile-generator/layers/obs_regions/obs_regions.yaml b/tile-generator/layers/obs_regions/obs_regions.yaml index 477bf45..2401923 100644 --- a/tile-generator/layers/obs_regions/obs_regions.yaml +++ b/tile-generator/layers/obs_regions/obs_regions.yaml @@ -8,8 +8,6 @@ layer: 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: @@ -17,7 +15,7 @@ layer: 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 + query: (SELECT region_id, geometry, name, overtaking_event_count FROM layer_obs_regions(!bbox!, z(!scale_denominator!))) AS t schema: - ./layer.sql