Use NUTS for region import, not OSM
This commit is contained in:
parent
0d9ddf4884
commit
ce8054b7ae
|
@ -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),
|
||||
)
|
|
@ -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);"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -17,3 +17,4 @@ osmium~=3.6.0
|
|||
psycopg~=3.1.8
|
||||
shapely~=2.0.1
|
||||
pyproj~=3.4.1
|
||||
aiohttp~=3.8.1
|
||||
|
|
|
@ -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")
|
||||
|
|
93
api/tools/import_regions.py
Executable file
93
api/tools/import_regions.py
Executable file
|
@ -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())
|
|
@ -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]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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({
|
|||
<Route path="/export" exact>
|
||||
<ExportPage />
|
||||
</Route>
|
||||
<Route path="/acknowledgements" exact>
|
||||
<AcknowledgementsPage />
|
||||
</Route>
|
||||
<Route path="/redirect" exact>
|
||||
<LoginRedirectPage />
|
||||
</Route>
|
||||
|
@ -280,13 +284,17 @@ const App = connect((state) => ({ login: state.login }))(function App({
|
|||
{t("App.footer.imprint")}
|
||||
</a>
|
||||
</List.Item>
|
||||
{ config?.termsUrl &&
|
||||
<List.Item>
|
||||
<a href={config?.termsUrl} target="_blank" rel="noreferrer">
|
||||
{t('App.footer.terms')}
|
||||
</a>
|
||||
</List.Item>
|
||||
}
|
||||
{config?.termsUrl && (
|
||||
<List.Item>
|
||||
<a
|
||||
href={config?.termsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t("App.footer.terms")}
|
||||
</a>
|
||||
</List.Item>
|
||||
)}
|
||||
<List.Item>
|
||||
<a
|
||||
href={`https://github.com/openbikesensor/portal${
|
||||
|
|
|
@ -1,189 +1,201 @@
|
|||
import _ from 'lodash'
|
||||
import produce from 'immer'
|
||||
import _ from "lodash";
|
||||
import produce from "immer";
|
||||
|
||||
import bright from './bright.json'
|
||||
import positron from './positron.json'
|
||||
import bright from "./bright.json";
|
||||
import positron from "./positron.json";
|
||||
|
||||
import viridisBase from 'colormap/res/res/viridis'
|
||||
import viridisBase from "colormap/res/res/viridis";
|
||||
|
||||
export {bright, positron}
|
||||
export const baseMapStyles = {bright, positron}
|
||||
export { bright, positron };
|
||||
export const baseMapStyles = { bright, positron };
|
||||
|
||||
function simplifyColormap(colormap, maxCount = 16) {
|
||||
const result = []
|
||||
const step = Math.ceil(colormap.length / maxCount)
|
||||
const result = [];
|
||||
const step = Math.ceil(colormap.length / maxCount);
|
||||
for (let i = 0; i < colormap.length; i += step) {
|
||||
result.push(colormap[i])
|
||||
result.push(colormap[i]);
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
function rgbArrayToColor(arr) {
|
||||
return ['rgb', ...arr.map((v) => 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;
|
||||
|
|
18
frontend/src/pages/AcknowledgementsPage.tsx
Normal file
18
frontend/src/pages/AcknowledgementsPage.tsx
Normal file
|
@ -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 (
|
||||
<Page title={title}>
|
||||
<Header as="h2">{title}</Header>
|
||||
<Markdown>{t("AcknowledgementsPage.information")}</Markdown>
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -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 = (
|
||||
<List.Item className={styles.copyright}>
|
||||
{t("MapPage.sidebar.copyright.openStreetMap")}{" "}
|
||||
<Link to="/acknowledgements">
|
||||
{t("MapPage.sidebar.copyright.learnMore")}
|
||||
</Link>
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<List relaxed>
|
||||
<List.Item>
|
||||
<List.Header>{t('MapPage.sidebar.baseMap.style.label')}</List.Header>
|
||||
<List.Header>{t("MapPage.sidebar.baseMap.style.label")}</List.Header>
|
||||
<Select
|
||||
options={BASEMAP_STYLE_OPTIONS.map((value) => ({
|
||||
value,
|
||||
|
@ -58,36 +83,47 @@ function LayerSidebar({
|
|||
text: t(`MapPage.sidebar.baseMap.style.${value}`),
|
||||
}))}
|
||||
value={style}
|
||||
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
|
||||
onChange={(_e, { value }) =>
|
||||
setMapConfigFlag("baseMap.style", value)
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
{openStreetMapCopyright}
|
||||
<Divider />
|
||||
<List.Item>
|
||||
<Checkbox
|
||||
toggle
|
||||
size="small"
|
||||
id="obsRegions.show"
|
||||
style={{float: 'right'}}
|
||||
style={{ float: "right" }}
|
||||
checked={showRegions}
|
||||
onChange={() => setMapConfigFlag('obsRegions.show', !showRegions)}
|
||||
onChange={() => setMapConfigFlag("obsRegions.show", !showRegions)}
|
||||
/>
|
||||
<label htmlFor="obsRegions.show">
|
||||
<Header as="h4">{t('MapPage.sidebar.obsRegions.title')}</Header>
|
||||
<Header as="h4">{t("MapPage.sidebar.obsRegions.title")}</Header>
|
||||
</label>
|
||||
</List.Item>
|
||||
{showRegions && (
|
||||
<>
|
||||
<List.Item>{t('MapPage.sidebar.obsRegions.colorByEventCount')}</List.Item>
|
||||
<List.Item>
|
||||
{t("MapPage.sidebar.obsRegions.colorByEventCount")}
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<ColorMapLegend
|
||||
twoTicks
|
||||
map={[
|
||||
[0, '#00897B00'],
|
||||
[5000, '#00897BFF'],
|
||||
[0, "#00897B00"],
|
||||
[5000, "#00897BFF"],
|
||||
]}
|
||||
digits={0}
|
||||
/>
|
||||
</List.Item>
|
||||
<List.Item className={styles.copyright}>
|
||||
{t("MapPage.sidebar.copyright.boundaries")}{" "}
|
||||
<Link to="/acknowledgements">
|
||||
{t("MapPage.sidebar.copyright.learnMore")}
|
||||
</Link>
|
||||
</List.Item>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
|
@ -96,12 +132,12 @@ function LayerSidebar({
|
|||
toggle
|
||||
size="small"
|
||||
id="obsRoads.show"
|
||||
style={{float: 'right'}}
|
||||
style={{ float: "right" }}
|
||||
checked={showRoads}
|
||||
onChange={() => setMapConfigFlag('obsRoads.show', !showRoads)}
|
||||
onChange={() => setMapConfigFlag("obsRoads.show", !showRoads)}
|
||||
/>
|
||||
<label htmlFor="obsRoads.show">
|
||||
<Header as="h4">{t('MapPage.sidebar.obsRoads.title')}</Header>
|
||||
<Header as="h4">{t("MapPage.sidebar.obsRoads.title")}</Header>
|
||||
</label>
|
||||
</List.Item>
|
||||
{showRoads && (
|
||||
|
@ -109,12 +145,16 @@ function LayerSidebar({
|
|||
<List.Item>
|
||||
<Checkbox
|
||||
checked={showUntagged}
|
||||
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
||||
label={t('MapPage.sidebar.obsRoads.showUntagged.label')}
|
||||
onChange={() =>
|
||||
setMapConfigFlag("obsRoads.showUntagged", !showUntagged)
|
||||
}
|
||||
label={t("MapPage.sidebar.obsRoads.showUntagged.label")}
|
||||
/>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Header>{t('MapPage.sidebar.obsRoads.attribute.label')}</List.Header>
|
||||
<List.Header>
|
||||
{t("MapPage.sidebar.obsRoads.attribute.label")}
|
||||
</List.Header>
|
||||
<Select
|
||||
fluid
|
||||
options={ROAD_ATTRIBUTE_OPTIONS.map((value) => ({
|
||||
|
@ -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)
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
{attribute.endsWith('_count') ? (
|
||||
{attribute.endsWith("_count") ? (
|
||||
<>
|
||||
<List.Item>
|
||||
<List.Header>{t('MapPage.sidebar.obsRoads.maxCount.label')}</List.Header>
|
||||
<List.Header>
|
||||
{t("MapPage.sidebar.obsRoads.maxCount.label")}
|
||||
</List.Header>
|
||||
<Input
|
||||
fluid
|
||||
type="number"
|
||||
value={maxCount}
|
||||
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)}
|
||||
onChange={(_e, { value }) =>
|
||||
setMapConfigFlag("obsRoads.maxCount", value)
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<ColorMapLegend
|
||||
map={_.chunk(
|
||||
colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml).slice(3),
|
||||
colorByCount(
|
||||
"obsRoads.maxCount",
|
||||
mapConfig.obsRoads.maxCount,
|
||||
viridisSimpleHtml
|
||||
).slice(3),
|
||||
2
|
||||
)}
|
||||
twoTicks
|
||||
/>
|
||||
</List.Item>
|
||||
</>
|
||||
) : attribute.endsWith('zone') ? (
|
||||
) : attribute.endsWith("zone") ? (
|
||||
<>
|
||||
<List.Item>
|
||||
<Label size="small" style={{background: 'blue', color: 'white'}}>
|
||||
{t('general.zone.urban')} (1.5 m)
|
||||
<Label
|
||||
size="small"
|
||||
style={{ background: "blue", color: "white" }}
|
||||
>
|
||||
{t("general.zone.urban")} (1.5 m)
|
||||
</Label>
|
||||
<Label size="small" style={{background: 'cyan', color: 'black'}}>
|
||||
{t('general.zone.rural')}(2 m)
|
||||
<Label
|
||||
size="small"
|
||||
style={{ background: "cyan", color: "black" }}
|
||||
>
|
||||
{t("general.zone.rural")}(2 m)
|
||||
</Label>
|
||||
</List.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<List.Item>
|
||||
<List.Header>{_.upperFirst(t('general.zone.urban'))}</List.Header>
|
||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
|
||||
<List.Header>
|
||||
{_.upperFirst(t("general.zone.urban"))}
|
||||
</List.Header>
|
||||
<DiscreteColorMapLegend
|
||||
map={colorByDistance("distance_overtaker")[3][5].slice(2)}
|
||||
/>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Header>{_.upperFirst(t('general.zone.rural'))}</List.Header>
|
||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
|
||||
<List.Header>
|
||||
{_.upperFirst(t("general.zone.rural"))}
|
||||
</List.Header>
|
||||
<DiscreteColorMapLegend
|
||||
map={colorByDistance("distance_overtaker")[3][3].slice(2)}
|
||||
/>
|
||||
</List.Item>
|
||||
</>
|
||||
)}
|
||||
{openStreetMapCopyright}
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
|
@ -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)}
|
||||
/>
|
||||
<label htmlFor="obsEvents.show">
|
||||
<Header as="h4">{t('MapPage.sidebar.obsEvents.title')}</Header>
|
||||
<Header as="h4">{t("MapPage.sidebar.obsEvents.title")}</Header>
|
||||
</label>
|
||||
</List.Item>
|
||||
{showEvents && (
|
||||
<>
|
||||
<List.Item>
|
||||
<List.Header>{_.upperFirst(t('general.zone.urban'))}</List.Header>
|
||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
|
||||
<List.Header>{_.upperFirst(t("general.zone.urban"))}</List.Header>
|
||||
<DiscreteColorMapLegend
|
||||
map={colorByDistance("distance_overtaker")[3][5].slice(2)}
|
||||
/>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Header>{_.upperFirst(t('general.zone.rural'))}</List.Header>
|
||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
|
||||
<List.Header>{_.upperFirst(t("general.zone.rural"))}</List.Header>
|
||||
<DiscreteColorMapLegend
|
||||
map={colorByDistance("distance_overtaker")[3][3].slice(2)}
|
||||
/>
|
||||
</List.Item>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<List.Item>
|
||||
<Header as="h4">{t('MapPage.sidebar.filters.title')}</Header>
|
||||
<Header as="h4">{t("MapPage.sidebar.filters.title")}</Header>
|
||||
</List.Item>
|
||||
|
||||
{login && (
|
||||
<>
|
||||
<List.Item>
|
||||
<Header as="h5">{t('MapPage.sidebar.filters.userData')}</Header>
|
||||
<Header as="h5">{t("MapPage.sidebar.filters.userData")}</Header>
|
||||
</List.Item>
|
||||
|
||||
<List.Item>
|
||||
|
@ -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")}
|
||||
/>
|
||||
</List.Item>
|
||||
|
||||
<List.Item>
|
||||
<Header as="h5">{t('MapPage.sidebar.filters.dateRange')}</Header>
|
||||
<Header as="h5">{t("MapPage.sidebar.filters.dateRange")}</Header>
|
||||
</List.Item>
|
||||
|
||||
<List.Item>
|
||||
|
@ -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)
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
|
||||
{dateMode == 'range' && (
|
||||
{dateMode == "range" && (
|
||||
<List.Item>
|
||||
<Input
|
||||
type="date"
|
||||
|
@ -246,14 +319,16 @@ function LayerSidebar({
|
|||
step="7"
|
||||
size="small"
|
||||
id="filters.startDate"
|
||||
onChange={(_e, {value}) => 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")}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
{dateMode == 'range' && (
|
||||
{dateMode == "range" && (
|
||||
<List.Item>
|
||||
<Input
|
||||
type="date"
|
||||
|
@ -261,14 +336,16 @@ function LayerSidebar({
|
|||
step="7"
|
||||
size="small"
|
||||
id="filters.endDate"
|
||||
onChange={(_e, {value}) => 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")}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
{dateMode == 'threshold' && (
|
||||
{dateMode == "threshold" && (
|
||||
<List.Item>
|
||||
<Input
|
||||
type="date"
|
||||
|
@ -277,33 +354,42 @@ function LayerSidebar({
|
|||
size="small"
|
||||
id="filters.startDate"
|
||||
value={startDate ?? null}
|
||||
onChange={(_e, {value}) => setMapConfigFlag('filters.startDate', value)}
|
||||
label={t('MapPage.sidebar.filters.threshold')}
|
||||
onChange={(_e, { value }) =>
|
||||
setMapConfigFlag("filters.startDate", value)
|
||||
}
|
||||
label={t("MapPage.sidebar.filters.threshold")}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
{dateMode == 'threshold' && (
|
||||
{dateMode == "threshold" && (
|
||||
<List.Item>
|
||||
<span>
|
||||
{t('MapPage.sidebar.filters.before')}{' '}
|
||||
{t("MapPage.sidebar.filters.before")}{" "}
|
||||
<Checkbox
|
||||
toggle
|
||||
size="small"
|
||||
checked={thresholdAfter ?? false}
|
||||
onChange={() => setMapConfigFlag('filters.thresholdAfter', !thresholdAfter)}
|
||||
onChange={() =>
|
||||
setMapConfigFlag(
|
||||
"filters.thresholdAfter",
|
||||
!thresholdAfter
|
||||
)
|
||||
}
|
||||
id="filters.thresholdAfter"
|
||||
/>{' '}
|
||||
{t('MapPage.sidebar.filters.after')}
|
||||
/>{" "}
|
||||
{t("MapPage.sidebar.filters.after")}
|
||||
</span>
|
||||
</List.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!login && <List.Item>{t('MapPage.sidebar.filters.needsLogin')}</List.Item>}
|
||||
{!login && (
|
||||
<List.Item>{t("MapPage.sidebar.filters.needsLogin")}</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
@ -316,6 +402,6 @@ export default connect(
|
|||
),
|
||||
login: state.login,
|
||||
}),
|
||||
{setMapConfigFlag: setMapConfigFlagAction}
|
||||
{ setMapConfigFlag: setMapConfigFlagAction }
|
||||
//
|
||||
)(LayerSidebar)
|
||||
)(LayerSidebar);
|
||||
|
|
|
@ -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 | Details>(null)
|
||||
function MapPage({ login }) {
|
||||
const { obsMapSource, banner } = useConfig() || {};
|
||||
const [details, setDetails] = useState<null | Details>(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 (
|
||||
<Page fullScreen title="Map">
|
||||
<div className={classNames(styles.mapContainer, banner ? styles.hasBanner : null)} ref={mapInfoPortal}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.mapContainer,
|
||||
banner ? styles.hasBanner : null
|
||||
)}
|
||||
ref={mapInfoPortal}
|
||||
>
|
||||
{layerSidebar && (
|
||||
<div className={styles.mapSidebar}>
|
||||
<LayerSidebar />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.map}>
|
||||
<Map viewportFromUrl onClick={onClick} hasToolbar onViewportChange={onViewportChange}>
|
||||
<Map
|
||||
viewportFromUrl
|
||||
onClick={onClick}
|
||||
hasToolbar
|
||||
onViewportChange={onViewportChange}
|
||||
>
|
||||
<div className={styles.mapToolbar}>
|
||||
<Button primary icon="bars" active={layerSidebar} onClick={onToggleLayerSidebarButtonClick} />
|
||||
<Button
|
||||
primary
|
||||
icon="bars"
|
||||
active={layerSidebar}
|
||||
onClick={onToggleLayerSidebarButtonClick}
|
||||
/>
|
||||
</div>
|
||||
<Source id="obs" {...obsMapSource} tiles={tiles}>
|
||||
{layers.map((layer) => (
|
||||
|
@ -255,23 +298,27 @@ function MapPage({login}) {
|
|||
))}
|
||||
</Source>
|
||||
|
||||
{details?.type === 'road' && details?.road?.road && (
|
||||
{details?.type === "road" && details?.road?.road && (
|
||||
<RoadInfo
|
||||
roadInfo={details.road}
|
||||
mapInfoPortal={mapInfoPortal.current}
|
||||
onClose={onCloseDetails}
|
||||
{...{hasFilters}}
|
||||
{...{ hasFilters }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{details?.type === 'region' && details?.region && (
|
||||
<RegionInfo region={details.region} mapInfoPortal={mapInfoPortal.current} onClose={onCloseDetails} />
|
||||
{details?.type === "region" && details?.region && (
|
||||
<RegionInfo
|
||||
region={details.region}
|
||||
mapInfoPortal={mapInfoPortal.current}
|
||||
onClose={onCloseDetails}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => ({login: state.login}))(MapPage)
|
||||
export default connect((state) => ({ login: state.login }))(MapPage);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue