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():
|
def upgrade():
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"region",
|
"region",
|
||||||
sa.Column(
|
sa.Column("id", sa.String(24), primary_key=True, index=True),
|
||||||
"relation_id", sa.BIGINT, primary_key=True, index=True, autoincrement=False
|
|
||||||
),
|
|
||||||
sa.Column("name", sa.Text),
|
sa.Column("name", sa.Text),
|
||||||
sa.Column("geometry", dbtype("GEOMETRY(GEOMETRY,3857)"), index=False),
|
sa.Column("geometry", dbtype("GEOMETRY(GEOMETRY,3857)"), index=False),
|
||||||
sa.Column("admin_level", sa.Integer, index=True),
|
sa.Column("admin_level", sa.Integer, index=True),
|
||||||
sa.Column("tags", dbtype("HSTORE")),
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
"CREATE INDEX region_geometry_idx ON region USING GIST (geometry) WITH (FILLFACTOR=100);"
|
"CREATE INDEX region_geometry_idx ON region USING GIST (geometry) WITH (FILLFACTOR=100);"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""add import groups
|
"""add import groups
|
||||||
|
|
||||||
Revision ID: b8b0fbae50a4
|
Revision ID: b8b0fbae50a4
|
||||||
Revises: 5c7755ead95d
|
Revises: f7b21148126a
|
||||||
Create Date: 2023-03-26 09:41:36.621203
|
Create Date: 2023-03-26 09:41:36.621203
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -11,7 +11,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = "b8b0fbae50a4"
|
revision = "b8b0fbae50a4"
|
||||||
down_revision = "5c7755ead95d"
|
down_revision = "f7b21148126a"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,7 @@ depends_on = None
|
||||||
|
|
||||||
def upgrade():
|
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_road_way_id ON road (way_id);")
|
||||||
op.execute(
|
op.execute("CREATE INDEX IF NOT EXISTS ix_region_id ON region (relation_id);")
|
||||||
"CREATE INDEX IF NOT EXISTS ix_region_relation_id ON region (relation_id);"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
|
|
@ -504,7 +504,7 @@ class Comment(Base):
|
||||||
class Region(Base):
|
class Region(Base):
|
||||||
__tablename__ = "region"
|
__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)
|
name = Column(Text)
|
||||||
geometry = Column(GeometryGeometry)
|
geometry = Column(GeometryGeometry)
|
||||||
admin_level = Column(Integer, index=True)
|
admin_level = Column(Integer, index=True)
|
||||||
|
|
|
@ -183,7 +183,7 @@ async def stats(req):
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
[
|
[
|
||||||
Region.relation_id.label("id"),
|
Region.id,
|
||||||
Region.name,
|
Region.name,
|
||||||
func.count(OvertakingEvent.id).label("overtaking_event_count"),
|
func.count(OvertakingEvent.id).label("overtaking_event_count"),
|
||||||
]
|
]
|
||||||
|
@ -195,12 +195,9 @@ async def stats(req):
|
||||||
func.ST_Transform(OvertakingEvent.geometry, 3857), Region.geometry
|
func.ST_Transform(OvertakingEvent.geometry, 3857), Region.geometry
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where(Region.admin_level == 6)
|
|
||||||
.group_by(
|
.group_by(
|
||||||
Region.relation_id,
|
Region.id,
|
||||||
Region.name,
|
Region.name,
|
||||||
Region.relation_id,
|
|
||||||
Region.admin_level,
|
|
||||||
Region.geometry,
|
Region.geometry,
|
||||||
)
|
)
|
||||||
.having(func.count(OvertakingEvent.id) > 0)
|
.having(func.count(OvertakingEvent.id) > 0)
|
||||||
|
|
|
@ -17,3 +17,4 @@ osmium~=3.6.0
|
||||||
psycopg~=3.1.8
|
psycopg~=3.1.8
|
||||||
shapely~=2.0.1
|
shapely~=2.0.1
|
||||||
pyproj~=3.4.1
|
pyproj~=3.4.1
|
||||||
|
aiohttp~=3.8.1
|
||||||
|
|
|
@ -18,17 +18,6 @@ log = logging.getLogger(__name__)
|
||||||
ROAD_BUFFER = 1000
|
ROAD_BUFFER = 1000
|
||||||
AREA_BUFFER = 100
|
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
|
@dataclass
|
||||||
class Road:
|
class Road:
|
||||||
|
@ -40,12 +29,9 @@ class Road:
|
||||||
geometry: bytes
|
geometry: bytes
|
||||||
|
|
||||||
|
|
||||||
data_types = {ROAD_TYPE: Road, REGION_TYPE: Region}
|
def read_file(filename):
|
||||||
|
|
||||||
|
|
||||||
def read_file(filename, only_type: bytes):
|
|
||||||
"""
|
"""
|
||||||
Reads a file iteratively, yielding Road and Region objects as they
|
Reads a file iteratively, yielding
|
||||||
appear. Those may be mixed.
|
appear. Those may be mixed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -53,11 +39,10 @@ def read_file(filename, only_type: bytes):
|
||||||
unpacker = msgpack.Unpacker(f)
|
unpacker = msgpack.Unpacker(f)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
type_id = unpacker.unpack()
|
type_id, *data = unpacker.unpack()
|
||||||
data = unpacker.unpack()
|
|
||||||
|
|
||||||
if type_id == only_type:
|
if type_id == b"\x01":
|
||||||
yield data_types[only_type](*data)
|
yield Road(*data)
|
||||||
|
|
||||||
except msgpack.OutOfData:
|
except msgpack.OutOfData:
|
||||||
pass
|
pass
|
||||||
|
@ -69,11 +54,8 @@ async def import_osm(connection, filename, import_group=None):
|
||||||
|
|
||||||
# Pass 1: Find IDs only
|
# Pass 1: Find IDs only
|
||||||
road_ids = []
|
road_ids = []
|
||||||
region_ids = []
|
for item in read_file(filename):
|
||||||
for item in read_file(filename, only_type=ROAD_TYPE):
|
|
||||||
road_ids.append(item.way_id)
|
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:
|
async with connection.cursor() as cursor:
|
||||||
log.info("Pass 1: Delete previously imported data")
|
log.info("Pass 1: Delete previously imported data")
|
||||||
|
@ -82,25 +64,17 @@ async def import_osm(connection, filename, import_group=None):
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"DELETE FROM road WHERE import_group = %s", (import_group,)
|
"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")
|
log.debug("Delete roads by way_id")
|
||||||
for ids in chunk(road_ids, 10000):
|
for ids in chunk(road_ids, 10000):
|
||||||
await cursor.execute("DELETE FROM road WHERE way_id = ANY(%s)", (ids,))
|
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
|
# Pass 2: Import
|
||||||
log.info("Pass 2: Import roads")
|
log.info("Pass 2: Import roads")
|
||||||
async with cursor.copy(
|
async with cursor.copy(
|
||||||
"COPY road (way_id, name, zone, directionality, oneway, geometry, import_group) FROM STDIN"
|
"COPY road (way_id, name, zone, directionality, oneway, geometry, import_group) FROM STDIN"
|
||||||
) as copy:
|
) as copy:
|
||||||
for item in read_file(filename, ROAD_TYPE):
|
for item in read_file(filename):
|
||||||
await copy.write_row(
|
await copy.write_row(
|
||||||
(
|
(
|
||||||
item.way_id,
|
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():
|
async def main():
|
||||||
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
|
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 = wkb.loads(wkbfab.create_linestring(way), hex=True)
|
||||||
geometry = transform(project, geometry)
|
geometry = transform(project, geometry)
|
||||||
geometry = wkb.dumps(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(
|
self.packer.pack(
|
||||||
[
|
[b"\x01", way.id, name, zone, directionality, oneway, geometry]
|
||||||
area.id,
|
|
||||||
name,
|
|
||||||
admin_level,
|
|
||||||
geometry,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import styles from "./App.module.less";
|
||||||
import { AVAILABLE_LOCALES, setLocale } from "i18n";
|
import { AVAILABLE_LOCALES, setLocale } from "i18n";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AcknowledgementsPage,
|
||||||
ExportPage,
|
ExportPage,
|
||||||
HomePage,
|
HomePage,
|
||||||
LoginRedirectPage,
|
LoginRedirectPage,
|
||||||
|
@ -186,6 +187,9 @@ const App = connect((state) => ({ login: state.login }))(function App({
|
||||||
<Route path="/export" exact>
|
<Route path="/export" exact>
|
||||||
<ExportPage />
|
<ExportPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/acknowledgements" exact>
|
||||||
|
<AcknowledgementsPage />
|
||||||
|
</Route>
|
||||||
<Route path="/redirect" exact>
|
<Route path="/redirect" exact>
|
||||||
<LoginRedirectPage />
|
<LoginRedirectPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -280,13 +284,17 @@ const App = connect((state) => ({ login: state.login }))(function App({
|
||||||
{t("App.footer.imprint")}
|
{t("App.footer.imprint")}
|
||||||
</a>
|
</a>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
{ config?.termsUrl &&
|
{config?.termsUrl && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<a href={config?.termsUrl} target="_blank" rel="noreferrer">
|
<a
|
||||||
{t('App.footer.terms')}
|
href={config?.termsUrl}
|
||||||
</a>
|
target="_blank"
|
||||||
</List.Item>
|
rel="noreferrer"
|
||||||
}
|
>
|
||||||
|
{t("App.footer.terms")}
|
||||||
|
</a>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/openbikesensor/portal${
|
href={`https://github.com/openbikesensor/portal${
|
||||||
|
|
|
@ -1,189 +1,201 @@
|
||||||
import _ from 'lodash'
|
import _ from "lodash";
|
||||||
import produce from 'immer'
|
import produce from "immer";
|
||||||
|
|
||||||
import bright from './bright.json'
|
import bright from "./bright.json";
|
||||||
import positron from './positron.json'
|
import positron from "./positron.json";
|
||||||
|
|
||||||
import viridisBase from 'colormap/res/res/viridis'
|
import viridisBase from "colormap/res/res/viridis";
|
||||||
|
|
||||||
export {bright, positron}
|
export { bright, positron };
|
||||||
export const baseMapStyles = {bright, positron}
|
export const baseMapStyles = { bright, positron };
|
||||||
|
|
||||||
function simplifyColormap(colormap, maxCount = 16) {
|
function simplifyColormap(colormap, maxCount = 16) {
|
||||||
const result = []
|
const result = [];
|
||||||
const step = Math.ceil(colormap.length / maxCount)
|
const step = Math.ceil(colormap.length / maxCount);
|
||||||
for (let i = 0; i < colormap.length; i += step) {
|
for (let i = 0; i < colormap.length; i += step) {
|
||||||
result.push(colormap[i])
|
result.push(colormap[i]);
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rgbArrayToColor(arr) {
|
function rgbArrayToColor(arr) {
|
||||||
return ['rgb', ...arr.map((v) => Math.round(v * 255))]
|
return ["rgb", ...arr.map((v) => Math.round(v * 255))];
|
||||||
}
|
}
|
||||||
|
|
||||||
function rgbArrayToHtml(arr) {
|
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) {
|
export function colormapToScale(colormap, value, min, max) {
|
||||||
return [
|
return [
|
||||||
'interpolate-hcl',
|
"interpolate-hcl",
|
||||||
['linear'],
|
["linear"],
|
||||||
value,
|
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 viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20);
|
||||||
export const viridisSimpleHtml = simplifyColormap(viridisBase.map(rgbArrayToHtml), 10)
|
export const viridisSimpleHtml = simplifyColormap(
|
||||||
export const grayscale = ['#FFFFFF', '#000000']
|
viridisBase.map(rgbArrayToHtml),
|
||||||
export const reds = [
|
10
|
||||||
'rgba( 255, 0, 0, 0)',
|
);
|
||||||
'rgba( 255, 0, 0, 255)',
|
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) {
|
export function colorByCount(
|
||||||
return colormapToScale(colormap, ['case', isValidAttribute(attribute), ['get', attribute], 0], 0, maxCount)
|
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],
|
var steps = { rural: [1.6, 1.8, 2.0, 2.2], urban: [1.1, 1.3, 1.5, 1.7] };
|
||||||
'urban': [1.1,1.3,1.5,1.7]}
|
|
||||||
|
|
||||||
export function isValidAttribute(attribute) {
|
export function isValidAttribute(attribute) {
|
||||||
if (attribute.endsWith('zone')) {
|
if (attribute.endsWith("zone")) {
|
||||||
return ['in', ['get', attribute], ['literal', ['rural', 'urban']]]
|
return ["in", ["get", attribute], ["literal", ["rural", "urban"]]];
|
||||||
}
|
}
|
||||||
return ['to-boolean', ['get', attribute]]
|
return ["to-boolean", ["get", attribute]];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function borderByZone() {
|
export function borderByZone() {
|
||||||
return ["match", ['get', 'zone'],
|
return ["match", ["get", "zone"], "rural", "cyan", "urban", "blue", "purple"];
|
||||||
"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 [
|
return [
|
||||||
'case',
|
"case",
|
||||||
['!', isValidAttribute(attribute)],
|
["!", isValidAttribute(attribute)],
|
||||||
fallback,
|
fallback,
|
||||||
["match", ['get', 'zone'], "rural",
|
|
||||||
[
|
[
|
||||||
'step',
|
"match",
|
||||||
['get', attribute],
|
["get", "zone"],
|
||||||
'rgba(150, 0, 0, 1)',
|
"rural",
|
||||||
steps['rural'][0],
|
[
|
||||||
'rgba(255, 0, 0, 1)',
|
"step",
|
||||||
steps['rural'][1],
|
["get", attribute],
|
||||||
'rgba(255, 220, 0, 1)',
|
"rgba(150, 0, 0, 1)",
|
||||||
steps['rural'][2],
|
steps["rural"][0],
|
||||||
'rgba(67, 200, 0, 1)',
|
"rgba(255, 0, 0, 1)",
|
||||||
steps['rural'][3],
|
steps["rural"][1],
|
||||||
'rgba(67, 150, 0, 1)',
|
"rgba(255, 220, 0, 1)",
|
||||||
], "urban",
|
steps["rural"][2],
|
||||||
[
|
"rgba(67, 200, 0, 1)",
|
||||||
'step',
|
steps["rural"][3],
|
||||||
['get', attribute],
|
"rgba(67, 150, 0, 1)",
|
||||||
'rgba(150, 0, 0, 1)',
|
],
|
||||||
steps['urban'][0],
|
"urban",
|
||||||
'rgba(255, 0, 0, 1)',
|
[
|
||||||
steps['urban'][1],
|
"step",
|
||||||
'rgba(255, 220, 0, 1)',
|
["get", attribute],
|
||||||
steps['urban'][2],
|
"rgba(150, 0, 0, 1)",
|
||||||
'rgba(67, 200, 0, 1)',
|
steps["urban"][0],
|
||||||
steps['urban'][3],
|
"rgba(255, 0, 0, 1)",
|
||||||
'rgba(67, 150, 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 = {
|
export const trackLayer = {
|
||||||
type: 'line',
|
type: "line",
|
||||||
paint: {
|
paint: {
|
||||||
'line-width': ['interpolate', ['linear'], ['zoom'], 14, 2, 17, 5],
|
"line-width": ["interpolate", ["linear"], ["zoom"], 14, 2, 17, 5],
|
||||||
'line-color': '#F06292',
|
"line-color": "#F06292",
|
||||||
'line-opacity': 0.6,
|
"line-opacity": 0.6,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getRegionLayers = (adminLevel = 6, baseColor = "#00897B", maxValue = 5000) => [{
|
export const getRegionLayers = (
|
||||||
id: 'region',
|
adminLevel = 6,
|
||||||
"type": "fill",
|
baseColor = "#00897B",
|
||||||
"source": "obs",
|
maxValue = 5000
|
||||||
"source-layer": "obs_regions",
|
) => [
|
||||||
"minzoom": 0,
|
{
|
||||||
"maxzoom": 10,
|
id: "region",
|
||||||
"filter": [
|
type: "fill",
|
||||||
"all",
|
source: "obs",
|
||||||
["==", "admin_level", adminLevel],
|
"source-layer": "obs_regions",
|
||||||
[">", "overtaking_event_count", 0],
|
minzoom: 0,
|
||||||
],
|
maxzoom: 10,
|
||||||
"paint": {
|
// filter: [">", "overtaking_event_count", 0],
|
||||||
"fill-color": baseColor,
|
paint: {
|
||||||
"fill-antialias": true,
|
"fill-color": baseColor,
|
||||||
"fill-opacity": [
|
"fill-antialias": true,
|
||||||
"interpolate",
|
"fill-opacity": [
|
||||||
["linear"],
|
"interpolate",
|
||||||
[
|
["linear"],
|
||||||
"log10",
|
["log10", ["get", "overtaking_event_count"]],
|
||||||
[
|
0,
|
||||||
"get",
|
0,
|
||||||
"overtaking_event_count"
|
Math.log10(maxValue),
|
||||||
]
|
0.9,
|
||||||
],
|
],
|
||||||
0,
|
},
|
||||||
0,
|
|
||||||
Math.log10(maxValue),
|
|
||||||
0.9
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: "region-border",
|
||||||
id: 'region-border',
|
type: "line",
|
||||||
"type": "line",
|
source: "obs",
|
||||||
"source": "obs",
|
"source-layer": "obs_regions",
|
||||||
"source-layer": "obs_regions",
|
minzoom: 0,
|
||||||
"minzoom": 0,
|
maxzoom: 10,
|
||||||
"maxzoom": 10,
|
// filter: [">", "overtaking_event_count", 0],
|
||||||
"filter": [
|
paint: {
|
||||||
"all",
|
"line-width": 1,
|
||||||
["==", "admin_level", adminLevel],
|
"line-color": baseColor,
|
||||||
[">", "overtaking_event_count", 0],
|
},
|
||||||
],
|
layout: {
|
||||||
"paint": {
|
"line-join": "round",
|
||||||
"line-width": 1,
|
"line-cap": "round",
|
||||||
"line-color": baseColor,
|
},
|
||||||
},
|
},
|
||||||
"layout": {
|
];
|
||||||
"line-join": "round",
|
|
||||||
"line-cap": "round"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
export const trackLayerRaw = produce(trackLayer, draft => {
|
export const trackLayerRaw = produce(trackLayer, (draft) => {
|
||||||
// draft.paint['line-color'] = '#81D4FA'
|
// draft.paint['line-color'] = '#81D4FA'
|
||||||
draft.paint['line-width'][4] = 1
|
draft.paint["line-width"][4] = 1;
|
||||||
draft.paint['line-width'][6] = 2
|
draft.paint["line-width"][6] = 2;
|
||||||
draft.paint['line-dasharray'] = [3, 3]
|
draft.paint["line-dasharray"] = [3, 3];
|
||||||
delete draft.paint['line-opacity']
|
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 React from "react";
|
||||||
import _ from 'lodash'
|
import _ from "lodash";
|
||||||
import {connect} from 'react-redux'
|
import { connect } from "react-redux";
|
||||||
import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react'
|
import { Link } from "react-router-dom";
|
||||||
import {useTranslation} from 'react-i18next'
|
import {
|
||||||
|
List,
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Checkbox,
|
||||||
|
Header,
|
||||||
|
} from "semantic-ui-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MapConfig,
|
MapConfig,
|
||||||
setMapConfigFlag as setMapConfigFlagAction,
|
setMapConfigFlag as setMapConfigFlagAction,
|
||||||
initialState as defaultMapConfig,
|
initialState as defaultMapConfig,
|
||||||
} from 'reducers/mapConfig'
|
} from "reducers/mapConfig";
|
||||||
import {colorByDistance, colorByCount, viridisSimpleHtml} from 'mapstyles'
|
import { colorByDistance, colorByCount, viridisSimpleHtml } from "mapstyles";
|
||||||
import {ColorMapLegend, DiscreteColorMapLegend} from 'components'
|
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 = [
|
const ROAD_ATTRIBUTE_OPTIONS = [
|
||||||
'distance_overtaker_mean',
|
"distance_overtaker_mean",
|
||||||
'distance_overtaker_min',
|
"distance_overtaker_min",
|
||||||
'distance_overtaker_max',
|
"distance_overtaker_max",
|
||||||
'distance_overtaker_median',
|
"distance_overtaker_median",
|
||||||
'overtaking_event_count',
|
"overtaking_event_count",
|
||||||
'usage_count',
|
"usage_count",
|
||||||
'zone',
|
"zone",
|
||||||
]
|
];
|
||||||
|
|
||||||
const DATE_FILTER_MODES = ['none', 'range', 'threshold']
|
const DATE_FILTER_MODES = ["none", "range", "threshold"];
|
||||||
|
|
||||||
type User = Object
|
type User = Object;
|
||||||
|
|
||||||
function LayerSidebar({
|
function LayerSidebar({
|
||||||
mapConfig,
|
mapConfig,
|
||||||
login,
|
login,
|
||||||
setMapConfigFlag,
|
setMapConfigFlag,
|
||||||
}: {
|
}: {
|
||||||
login: User | null
|
login: User | null;
|
||||||
mapConfig: MapConfig
|
mapConfig: MapConfig;
|
||||||
setMapConfigFlag: (flag: string, value: unknown) => void
|
setMapConfigFlag: (flag: string, value: unknown) => void;
|
||||||
}) {
|
}) {
|
||||||
const {t} = useTranslation()
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
baseMap: {style},
|
baseMap: { style },
|
||||||
obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
|
obsRoads: { show: showRoads, showUntagged, attribute, maxCount },
|
||||||
obsEvents: {show: showEvents},
|
obsEvents: { show: showEvents },
|
||||||
obsRegions: {show: showRegions},
|
obsRegions: { show: showRegions },
|
||||||
filters: {currentUser: filtersCurrentUser, dateMode, startDate, endDate, thresholdAfter},
|
filters: {
|
||||||
} = mapConfig
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<List relaxed>
|
<List relaxed>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{t('MapPage.sidebar.baseMap.style.label')}</List.Header>
|
<List.Header>{t("MapPage.sidebar.baseMap.style.label")}</List.Header>
|
||||||
<Select
|
<Select
|
||||||
options={BASEMAP_STYLE_OPTIONS.map((value) => ({
|
options={BASEMAP_STYLE_OPTIONS.map((value) => ({
|
||||||
value,
|
value,
|
||||||
|
@ -58,36 +83,47 @@ function LayerSidebar({
|
||||||
text: t(`MapPage.sidebar.baseMap.style.${value}`),
|
text: t(`MapPage.sidebar.baseMap.style.${value}`),
|
||||||
}))}
|
}))}
|
||||||
value={style}
|
value={style}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("baseMap.style", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
{openStreetMapCopyright}
|
||||||
<Divider />
|
<Divider />
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
toggle
|
toggle
|
||||||
size="small"
|
size="small"
|
||||||
id="obsRegions.show"
|
id="obsRegions.show"
|
||||||
style={{float: 'right'}}
|
style={{ float: "right" }}
|
||||||
checked={showRegions}
|
checked={showRegions}
|
||||||
onChange={() => setMapConfigFlag('obsRegions.show', !showRegions)}
|
onChange={() => setMapConfigFlag("obsRegions.show", !showRegions)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="obsRegions.show">
|
<label htmlFor="obsRegions.show">
|
||||||
<Header as="h4">{t('MapPage.sidebar.obsRegions.title')}</Header>
|
<Header as="h4">{t("MapPage.sidebar.obsRegions.title")}</Header>
|
||||||
</label>
|
</label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
{showRegions && (
|
{showRegions && (
|
||||||
<>
|
<>
|
||||||
<List.Item>{t('MapPage.sidebar.obsRegions.colorByEventCount')}</List.Item>
|
<List.Item>
|
||||||
|
{t("MapPage.sidebar.obsRegions.colorByEventCount")}
|
||||||
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<ColorMapLegend
|
<ColorMapLegend
|
||||||
twoTicks
|
twoTicks
|
||||||
map={[
|
map={[
|
||||||
[0, '#00897B00'],
|
[0, "#00897B00"],
|
||||||
[5000, '#00897BFF'],
|
[5000, "#00897BFF"],
|
||||||
]}
|
]}
|
||||||
digits={0}
|
digits={0}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
<List.Item className={styles.copyright}>
|
||||||
|
{t("MapPage.sidebar.copyright.boundaries")}{" "}
|
||||||
|
<Link to="/acknowledgements">
|
||||||
|
{t("MapPage.sidebar.copyright.learnMore")}
|
||||||
|
</Link>
|
||||||
|
</List.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -96,12 +132,12 @@ function LayerSidebar({
|
||||||
toggle
|
toggle
|
||||||
size="small"
|
size="small"
|
||||||
id="obsRoads.show"
|
id="obsRoads.show"
|
||||||
style={{float: 'right'}}
|
style={{ float: "right" }}
|
||||||
checked={showRoads}
|
checked={showRoads}
|
||||||
onChange={() => setMapConfigFlag('obsRoads.show', !showRoads)}
|
onChange={() => setMapConfigFlag("obsRoads.show", !showRoads)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="obsRoads.show">
|
<label htmlFor="obsRoads.show">
|
||||||
<Header as="h4">{t('MapPage.sidebar.obsRoads.title')}</Header>
|
<Header as="h4">{t("MapPage.sidebar.obsRoads.title")}</Header>
|
||||||
</label>
|
</label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
{showRoads && (
|
{showRoads && (
|
||||||
|
@ -109,12 +145,16 @@ function LayerSidebar({
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={showUntagged}
|
checked={showUntagged}
|
||||||
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
onChange={() =>
|
||||||
label={t('MapPage.sidebar.obsRoads.showUntagged.label')}
|
setMapConfigFlag("obsRoads.showUntagged", !showUntagged)
|
||||||
|
}
|
||||||
|
label={t("MapPage.sidebar.obsRoads.showUntagged.label")}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<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
|
<Select
|
||||||
fluid
|
fluid
|
||||||
options={ROAD_ATTRIBUTE_OPTIONS.map((value) => ({
|
options={ROAD_ATTRIBUTE_OPTIONS.map((value) => ({
|
||||||
|
@ -123,53 +163,78 @@ function LayerSidebar({
|
||||||
text: t(`MapPage.sidebar.obsRoads.attribute.${value}`),
|
text: t(`MapPage.sidebar.obsRoads.attribute.${value}`),
|
||||||
}))}
|
}))}
|
||||||
value={attribute}
|
value={attribute}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("obsRoads.attribute", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
{attribute.endsWith('_count') ? (
|
{attribute.endsWith("_count") ? (
|
||||||
<>
|
<>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{t('MapPage.sidebar.obsRoads.maxCount.label')}</List.Header>
|
<List.Header>
|
||||||
|
{t("MapPage.sidebar.obsRoads.maxCount.label")}
|
||||||
|
</List.Header>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
type="number"
|
type="number"
|
||||||
value={maxCount}
|
value={maxCount}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("obsRoads.maxCount", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<ColorMapLegend
|
<ColorMapLegend
|
||||||
map={_.chunk(
|
map={_.chunk(
|
||||||
colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml).slice(3),
|
colorByCount(
|
||||||
|
"obsRoads.maxCount",
|
||||||
|
mapConfig.obsRoads.maxCount,
|
||||||
|
viridisSimpleHtml
|
||||||
|
).slice(3),
|
||||||
2
|
2
|
||||||
)}
|
)}
|
||||||
twoTicks
|
twoTicks
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</>
|
</>
|
||||||
) : attribute.endsWith('zone') ? (
|
) : attribute.endsWith("zone") ? (
|
||||||
<>
|
<>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Label size="small" style={{background: 'blue', color: 'white'}}>
|
<Label
|
||||||
{t('general.zone.urban')} (1.5 m)
|
size="small"
|
||||||
|
style={{ background: "blue", color: "white" }}
|
||||||
|
>
|
||||||
|
{t("general.zone.urban")} (1.5 m)
|
||||||
</Label>
|
</Label>
|
||||||
<Label size="small" style={{background: 'cyan', color: 'black'}}>
|
<Label
|
||||||
{t('general.zone.rural')}(2 m)
|
size="small"
|
||||||
|
style={{ background: "cyan", color: "black" }}
|
||||||
|
>
|
||||||
|
{t("general.zone.rural")}(2 m)
|
||||||
</Label>
|
</Label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{_.upperFirst(t('general.zone.urban'))}</List.Header>
|
<List.Header>
|
||||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
|
{_.upperFirst(t("general.zone.urban"))}
|
||||||
|
</List.Header>
|
||||||
|
<DiscreteColorMapLegend
|
||||||
|
map={colorByDistance("distance_overtaker")[3][5].slice(2)}
|
||||||
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{_.upperFirst(t('general.zone.rural'))}</List.Header>
|
<List.Header>
|
||||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
|
{_.upperFirst(t("general.zone.rural"))}
|
||||||
|
</List.Header>
|
||||||
|
<DiscreteColorMapLegend
|
||||||
|
map={colorByDistance("distance_overtaker")[3][3].slice(2)}
|
||||||
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{openStreetMapCopyright}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -178,36 +243,40 @@ function LayerSidebar({
|
||||||
toggle
|
toggle
|
||||||
size="small"
|
size="small"
|
||||||
id="obsEvents.show"
|
id="obsEvents.show"
|
||||||
style={{float: 'right'}}
|
style={{ float: "right" }}
|
||||||
checked={showEvents}
|
checked={showEvents}
|
||||||
onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)}
|
onChange={() => setMapConfigFlag("obsEvents.show", !showEvents)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="obsEvents.show">
|
<label htmlFor="obsEvents.show">
|
||||||
<Header as="h4">{t('MapPage.sidebar.obsEvents.title')}</Header>
|
<Header as="h4">{t("MapPage.sidebar.obsEvents.title")}</Header>
|
||||||
</label>
|
</label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
{showEvents && (
|
{showEvents && (
|
||||||
<>
|
<>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{_.upperFirst(t('general.zone.urban'))}</List.Header>
|
<List.Header>{_.upperFirst(t("general.zone.urban"))}</List.Header>
|
||||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
|
<DiscreteColorMapLegend
|
||||||
|
map={colorByDistance("distance_overtaker")[3][5].slice(2)}
|
||||||
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>{_.upperFirst(t('general.zone.rural'))}</List.Header>
|
<List.Header>{_.upperFirst(t("general.zone.rural"))}</List.Header>
|
||||||
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
|
<DiscreteColorMapLegend
|
||||||
|
map={colorByDistance("distance_overtaker")[3][3].slice(2)}
|
||||||
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Header as="h4">{t('MapPage.sidebar.filters.title')}</Header>
|
<Header as="h4">{t("MapPage.sidebar.filters.title")}</Header>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
||||||
{login && (
|
{login && (
|
||||||
<>
|
<>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Header as="h5">{t('MapPage.sidebar.filters.userData')}</Header>
|
<Header as="h5">{t("MapPage.sidebar.filters.userData")}</Header>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
||||||
<List.Item>
|
<List.Item>
|
||||||
|
@ -216,13 +285,15 @@ function LayerSidebar({
|
||||||
size="small"
|
size="small"
|
||||||
id="filters.currentUser"
|
id="filters.currentUser"
|
||||||
checked={filtersCurrentUser}
|
checked={filtersCurrentUser}
|
||||||
onChange={() => setMapConfigFlag('filters.currentUser', !filtersCurrentUser)}
|
onChange={() =>
|
||||||
label={t('MapPage.sidebar.filters.currentUser')}
|
setMapConfigFlag("filters.currentUser", !filtersCurrentUser)
|
||||||
|
}
|
||||||
|
label={t("MapPage.sidebar.filters.currentUser")}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<List.Item>
|
<List.Item>
|
||||||
|
@ -233,12 +304,14 @@ function LayerSidebar({
|
||||||
key: value,
|
key: value,
|
||||||
text: t(`MapPage.sidebar.filters.dateMode.${value}`),
|
text: t(`MapPage.sidebar.filters.dateMode.${value}`),
|
||||||
}))}
|
}))}
|
||||||
value={dateMode ?? 'none'}
|
value={dateMode ?? "none"}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('filters.dateMode', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("filters.dateMode", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
||||||
{dateMode == 'range' && (
|
{dateMode == "range" && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
|
@ -246,14 +319,16 @@ function LayerSidebar({
|
||||||
step="7"
|
step="7"
|
||||||
size="small"
|
size="small"
|
||||||
id="filters.startDate"
|
id="filters.startDate"
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('filters.startDate', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("filters.startDate", value)
|
||||||
|
}
|
||||||
value={startDate ?? null}
|
value={startDate ?? null}
|
||||||
label={t('MapPage.sidebar.filters.start')}
|
label={t("MapPage.sidebar.filters.start")}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{dateMode == 'range' && (
|
{dateMode == "range" && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
|
@ -261,14 +336,16 @@ function LayerSidebar({
|
||||||
step="7"
|
step="7"
|
||||||
size="small"
|
size="small"
|
||||||
id="filters.endDate"
|
id="filters.endDate"
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('filters.endDate', value)}
|
onChange={(_e, { value }) =>
|
||||||
|
setMapConfigFlag("filters.endDate", value)
|
||||||
|
}
|
||||||
value={endDate ?? null}
|
value={endDate ?? null}
|
||||||
label={t('MapPage.sidebar.filters.end')}
|
label={t("MapPage.sidebar.filters.end")}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{dateMode == 'threshold' && (
|
{dateMode == "threshold" && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
|
@ -277,33 +354,42 @@ function LayerSidebar({
|
||||||
size="small"
|
size="small"
|
||||||
id="filters.startDate"
|
id="filters.startDate"
|
||||||
value={startDate ?? null}
|
value={startDate ?? null}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('filters.startDate', value)}
|
onChange={(_e, { value }) =>
|
||||||
label={t('MapPage.sidebar.filters.threshold')}
|
setMapConfigFlag("filters.startDate", value)
|
||||||
|
}
|
||||||
|
label={t("MapPage.sidebar.filters.threshold")}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{dateMode == 'threshold' && (
|
{dateMode == "threshold" && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<span>
|
<span>
|
||||||
{t('MapPage.sidebar.filters.before')}{' '}
|
{t("MapPage.sidebar.filters.before")}{" "}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
toggle
|
toggle
|
||||||
size="small"
|
size="small"
|
||||||
checked={thresholdAfter ?? false}
|
checked={thresholdAfter ?? false}
|
||||||
onChange={() => setMapConfigFlag('filters.thresholdAfter', !thresholdAfter)}
|
onChange={() =>
|
||||||
|
setMapConfigFlag(
|
||||||
|
"filters.thresholdAfter",
|
||||||
|
!thresholdAfter
|
||||||
|
)
|
||||||
|
}
|
||||||
id="filters.thresholdAfter"
|
id="filters.thresholdAfter"
|
||||||
/>{' '}
|
/>{" "}
|
||||||
{t('MapPage.sidebar.filters.after')}
|
{t("MapPage.sidebar.filters.after")}
|
||||||
</span>
|
</span>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!login && <List.Item>{t('MapPage.sidebar.filters.needsLogin')}</List.Item>}
|
{!login && (
|
||||||
|
<List.Item>{t("MapPage.sidebar.filters.needsLogin")}</List.Item>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -316,6 +402,6 @@ export default connect(
|
||||||
),
|
),
|
||||||
login: state.login,
|
login: state.login,
|
||||||
}),
|
}),
|
||||||
{setMapConfigFlag: setMapConfigFlagAction}
|
{ setMapConfigFlag: setMapConfigFlagAction }
|
||||||
//
|
//
|
||||||
)(LayerSidebar)
|
)(LayerSidebar);
|
||||||
|
|
|
@ -1,253 +1,296 @@
|
||||||
import React, {useState, useCallback, useMemo, useRef} from 'react'
|
import React, { useState, useCallback, useMemo, useRef } from "react";
|
||||||
import _ from 'lodash'
|
import _ from "lodash";
|
||||||
import {connect} from 'react-redux'
|
import { connect } from "react-redux";
|
||||||
import {Button} from 'semantic-ui-react'
|
import { Button } from "semantic-ui-react";
|
||||||
import {Layer, Source} from 'react-map-gl'
|
import { Layer, Source } from "react-map-gl";
|
||||||
import produce from 'immer'
|
import produce from "immer";
|
||||||
import classNames from 'classnames'
|
import classNames from "classnames";
|
||||||
|
|
||||||
import api from 'api'
|
import api from "api";
|
||||||
import type {Location} from 'types'
|
import type { Location } from "types";
|
||||||
import {Page, Map} from 'components'
|
import { Page, Map } from "components";
|
||||||
import {useConfig} from 'config'
|
import { useConfig } from "config";
|
||||||
import {colorByDistance, colorByCount, getRegionLayers, borderByZone, isValidAttribute} from 'mapstyles'
|
import {
|
||||||
import {useMapConfig} from 'reducers/mapConfig'
|
colorByDistance,
|
||||||
|
colorByCount,
|
||||||
|
getRegionLayers,
|
||||||
|
borderByZone,
|
||||||
|
isValidAttribute,
|
||||||
|
} from "mapstyles";
|
||||||
|
import { useMapConfig } from "reducers/mapConfig";
|
||||||
|
|
||||||
import RoadInfo, {RoadInfoType} from './RoadInfo'
|
import RoadInfo, { RoadInfoType } from "./RoadInfo";
|
||||||
import RegionInfo from './RegionInfo'
|
import RegionInfo from "./RegionInfo";
|
||||||
import LayerSidebar from './LayerSidebar'
|
import LayerSidebar from "./LayerSidebar";
|
||||||
import styles from './styles.module.less'
|
import styles from "./styles.module.less";
|
||||||
|
|
||||||
const untaggedRoadsLayer = {
|
const untaggedRoadsLayer = {
|
||||||
id: 'obs_roads_untagged',
|
id: "obs_roads_untagged",
|
||||||
type: 'line',
|
type: "line",
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_roads',
|
"source-layer": "obs_roads",
|
||||||
minzoom: 12,
|
minzoom: 12,
|
||||||
filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]],
|
filter: ["!", ["to-boolean", ["get", "distance_overtaker_mean"]]],
|
||||||
layout: {
|
layout: {
|
||||||
'line-cap': 'round',
|
"line-cap": "round",
|
||||||
'line-join': 'round',
|
"line-join": "round",
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
|
"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 2, 17, 2],
|
||||||
'line-color': '#ABC',
|
"line-color": "#ABC",
|
||||||
// "line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
|
// "line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
|
||||||
'line-offset': [
|
"line-offset": [
|
||||||
'interpolate',
|
"interpolate",
|
||||||
['exponential', 1.5],
|
["exponential", 1.5],
|
||||||
['zoom'],
|
["zoom"],
|
||||||
12,
|
12,
|
||||||
['get', 'offset_direction'],
|
["get", "offset_direction"],
|
||||||
19,
|
19,
|
||||||
['*', ['get', 'offset_direction'], 8],
|
["*", ["get", "offset_direction"], 8],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const getUntaggedRoadsLayer = (colorAttribute) =>
|
const getUntaggedRoadsLayer = (colorAttribute) =>
|
||||||
produce(untaggedRoadsLayer, (draft) => {
|
produce(untaggedRoadsLayer, (draft) => {
|
||||||
draft.filter = ['!', isValidAttribute(colorAttribute)]
|
draft.filter = ["!", isValidAttribute(colorAttribute)];
|
||||||
})
|
});
|
||||||
|
|
||||||
const getRoadsLayer = (colorAttribute, maxCount) =>
|
const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
produce(untaggedRoadsLayer, (draft) => {
|
produce(untaggedRoadsLayer, (draft) => {
|
||||||
draft.id = 'obs_roads_normal'
|
draft.id = "obs_roads_normal";
|
||||||
draft.filter = isValidAttribute(colorAttribute)
|
draft.filter = isValidAttribute(colorAttribute);
|
||||||
draft.minzoom = 10
|
draft.minzoom = 10;
|
||||||
draft.paint['line-width'][6] = 6 // scale bigger on zoom
|
draft.paint["line-width"][6] = 6; // scale bigger on zoom
|
||||||
draft.paint['line-color'] = colorAttribute.startsWith('distance_')
|
draft.paint["line-color"] = colorAttribute.startsWith("distance_")
|
||||||
? colorByDistance(colorAttribute)
|
? colorByDistance(colorAttribute)
|
||||||
: colorAttribute.endsWith('_count')
|
: colorAttribute.endsWith("_count")
|
||||||
? colorByCount(colorAttribute, maxCount)
|
? colorByCount(colorAttribute, maxCount)
|
||||||
: colorAttribute.endsWith('zone')
|
: colorAttribute.endsWith("zone")
|
||||||
? borderByZone()
|
? borderByZone()
|
||||||
: '#DDD'
|
: "#DDD";
|
||||||
// draft.paint["line-opacity"][3] = 12;
|
// draft.paint["line-opacity"][3] = 12;
|
||||||
// draft.paint["line-opacity"][5] = 13;
|
// draft.paint["line-opacity"][5] = 13;
|
||||||
})
|
});
|
||||||
|
|
||||||
const getEventsLayer = () => ({
|
const getEventsLayer = () => ({
|
||||||
id: 'obs_events',
|
id: "obs_events",
|
||||||
type: 'circle',
|
type: "circle",
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_events',
|
"source-layer": "obs_events",
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8],
|
"circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 3, 17, 8],
|
||||||
'circle-color': colorByDistance('distance_overtaker'),
|
"circle-color": colorByDistance("distance_overtaker"),
|
||||||
},
|
},
|
||||||
minzoom: 11,
|
minzoom: 8,
|
||||||
})
|
});
|
||||||
|
|
||||||
const getEventsTextLayer = () => ({
|
const getEventsTextLayer = () => ({
|
||||||
id: 'obs_events_text',
|
id: "obs_events_text",
|
||||||
type: 'symbol',
|
type: "symbol",
|
||||||
minzoom: 18,
|
minzoom: 18,
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_events',
|
"source-layer": "obs_events",
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': [
|
"text-field": [
|
||||||
'number-format',
|
"number-format",
|
||||||
['get', 'distance_overtaker'],
|
["get", "distance_overtaker"],
|
||||||
{'min-fraction-digits': 2, 'max-fraction-digits': 2},
|
{ "min-fraction-digits": 2, "max-fraction-digits": 2 },
|
||||||
],
|
],
|
||||||
'text-allow-overlap': true,
|
"text-allow-overlap": true,
|
||||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
|
"text-font": ["Open Sans Bold", "Arial Unicode MS Regular"],
|
||||||
'text-size': 14,
|
"text-size": 14,
|
||||||
'text-keep-upright': false,
|
"text-keep-upright": false,
|
||||||
'text-anchor': 'left',
|
"text-anchor": "left",
|
||||||
'text-radial-offset': 1,
|
"text-radial-offset": 1,
|
||||||
'text-rotate': ['-', 90, ['*', ['get', 'course'], 180 / Math.PI]],
|
"text-rotate": ["-", 90, ["*", ["get", "course"], 180 / Math.PI]],
|
||||||
'text-rotation-alignment': 'map',
|
"text-rotation-alignment": "map",
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-halo-color': 'rgba(255, 255, 255, 1)',
|
"text-halo-color": "rgba(255, 255, 255, 1)",
|
||||||
'text-halo-width': 1,
|
"text-halo-width": 1,
|
||||||
'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1],
|
"text-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.3, 1],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
interface RegionInfo {
|
interface RegionInfo {
|
||||||
properties: {
|
properties: {
|
||||||
admin_level: number
|
admin_level: number;
|
||||||
name: string
|
name: string;
|
||||||
overtaking_event_count: number
|
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}) {
|
function MapPage({ login }) {
|
||||||
const {obsMapSource, banner} = useConfig() || {}
|
const { obsMapSource, banner } = useConfig() || {};
|
||||||
const [details, setDetails] = useState<null | Details>(null)
|
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 viewportRef = useRef();
|
||||||
const mapInfoPortal = useRef()
|
const mapInfoPortal = useRef();
|
||||||
|
|
||||||
const onViewportChange = useCallback(
|
const onViewportChange = useCallback(
|
||||||
(viewport) => {
|
(viewport) => {
|
||||||
viewportRef.current = viewport
|
viewportRef.current = viewport;
|
||||||
},
|
},
|
||||||
[viewportRef]
|
[viewportRef]
|
||||||
)
|
);
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
async (e) => {
|
async (e) => {
|
||||||
// check if we clicked inside the mapInfoBox, if so, early exit
|
// check if we clicked inside the mapInfoBox, if so, early exit
|
||||||
let node = e.target
|
let node = e.target;
|
||||||
while (node) {
|
while (node) {
|
||||||
if ([styles.mapInfoBox, styles.mapToolbar].some((className) => node?.classList?.contains(className))) {
|
if (
|
||||||
return
|
[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) {
|
if (zoom < 10) {
|
||||||
const clickedRegion = e.features?.find((f) => f.source === 'obs' && f.sourceLayer === 'obs_regions')
|
const clickedRegion = e.features?.find(
|
||||||
setDetails(clickedRegion ? {type: 'region', region: clickedRegion} : null)
|
(f) => f.source === "obs" && f.sourceLayer === "obs_regions"
|
||||||
|
);
|
||||||
|
setDetails(
|
||||||
|
clickedRegion ? { type: "region", region: clickedRegion } : null
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const road = await api.get('/mapdetails/road', {
|
const road = await api.get("/mapdetails/road", {
|
||||||
query: {
|
query: {
|
||||||
longitude: e.lngLat[0],
|
longitude: e.lngLat[0],
|
||||||
latitude: e.lngLat[1],
|
latitude: e.lngLat[1],
|
||||||
radius: 100,
|
radius: 100,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
setDetails(road?.road ? {type: 'road', road} : null)
|
setDetails(road?.road ? { type: "road", road } : null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setDetails]
|
[setDetails]
|
||||||
)
|
);
|
||||||
|
|
||||||
const [layerSidebar, setLayerSidebar] = useState(true)
|
const [layerSidebar, setLayerSidebar] = useState(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
obsRoads: {attribute, maxCount},
|
obsRoads: { attribute, maxCount },
|
||||||
} = mapConfig
|
} = mapConfig;
|
||||||
|
|
||||||
const layers = []
|
const layers = [];
|
||||||
|
|
||||||
const untaggedRoadsLayerCustom = useMemo(() => getUntaggedRoadsLayer(attribute), [attribute])
|
const untaggedRoadsLayerCustom = useMemo(
|
||||||
|
() => getUntaggedRoadsLayer(attribute),
|
||||||
|
[attribute]
|
||||||
|
);
|
||||||
if (mapConfig.obsRoads.show && mapConfig.obsRoads.showUntagged) {
|
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) {
|
if (mapConfig.obsRoads.show) {
|
||||||
layers.push(roadsLayer)
|
layers.push(roadsLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const regionLayers = useMemo(() => getRegionLayers(), [])
|
const regionLayers = useMemo(() => getRegionLayers(), []);
|
||||||
if (mapConfig.obsRegions.show) {
|
if (mapConfig.obsRegions.show) {
|
||||||
layers.push(...regionLayers)
|
layers.push(...regionLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventsLayer = useMemo(() => getEventsLayer(), [])
|
const eventsLayer = useMemo(() => getEventsLayer(), []);
|
||||||
const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
|
const eventsTextLayer = useMemo(() => getEventsTextLayer(), []);
|
||||||
|
|
||||||
if (mapConfig.obsEvents.show) {
|
if (mapConfig.obsEvents.show) {
|
||||||
layers.push(eventsLayer)
|
layers.push(eventsLayer);
|
||||||
layers.push(eventsTextLayer)
|
layers.push(eventsTextLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onToggleLayerSidebarButtonClick = useCallback(
|
const onToggleLayerSidebarButtonClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
console.log('toggl;e')
|
console.log("toggl;e");
|
||||||
setLayerSidebar((v) => !v)
|
setLayerSidebar((v) => !v);
|
||||||
},
|
},
|
||||||
[setLayerSidebar]
|
[setLayerSidebar]
|
||||||
)
|
);
|
||||||
|
|
||||||
if (!obsMapSource) {
|
if (!obsMapSource) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tiles = obsMapSource?.tiles?.map((tileUrl: string) => {
|
const tiles = obsMapSource?.tiles?.map((tileUrl: string) => {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams();
|
||||||
if (login) {
|
if (login) {
|
||||||
if (mapConfig.filters.currentUser) {
|
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) {
|
if (mapConfig.filters.startDate) {
|
||||||
query.append('start', mapConfig.filters.startDate)
|
query.append("start", mapConfig.filters.startDate);
|
||||||
}
|
}
|
||||||
if (mapConfig.filters.endDate) {
|
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) {
|
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)
|
const queryString = String(query);
|
||||||
return tileUrl + (queryString ? '?' : '') + queryString
|
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 (
|
return (
|
||||||
<Page fullScreen title="Map">
|
<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 && (
|
{layerSidebar && (
|
||||||
<div className={styles.mapSidebar}>
|
<div className={styles.mapSidebar}>
|
||||||
<LayerSidebar />
|
<LayerSidebar />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.map}>
|
<div className={styles.map}>
|
||||||
<Map viewportFromUrl onClick={onClick} hasToolbar onViewportChange={onViewportChange}>
|
<Map
|
||||||
|
viewportFromUrl
|
||||||
|
onClick={onClick}
|
||||||
|
hasToolbar
|
||||||
|
onViewportChange={onViewportChange}
|
||||||
|
>
|
||||||
<div className={styles.mapToolbar}>
|
<div className={styles.mapToolbar}>
|
||||||
<Button primary icon="bars" active={layerSidebar} onClick={onToggleLayerSidebarButtonClick} />
|
<Button
|
||||||
|
primary
|
||||||
|
icon="bars"
|
||||||
|
active={layerSidebar}
|
||||||
|
onClick={onToggleLayerSidebarButtonClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Source id="obs" {...obsMapSource} tiles={tiles}>
|
<Source id="obs" {...obsMapSource} tiles={tiles}>
|
||||||
{layers.map((layer) => (
|
{layers.map((layer) => (
|
||||||
|
@ -255,23 +298,27 @@ function MapPage({login}) {
|
||||||
))}
|
))}
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{details?.type === 'road' && details?.road?.road && (
|
{details?.type === "road" && details?.road?.road && (
|
||||||
<RoadInfo
|
<RoadInfo
|
||||||
roadInfo={details.road}
|
roadInfo={details.road}
|
||||||
mapInfoPortal={mapInfoPortal.current}
|
mapInfoPortal={mapInfoPortal.current}
|
||||||
onClose={onCloseDetails}
|
onClose={onCloseDetails}
|
||||||
{...{hasFilters}}
|
{...{ hasFilters }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{details?.type === 'region' && details?.region && (
|
{details?.type === "region" && details?.region && (
|
||||||
<RegionInfo region={details.region} mapInfoPortal={mapInfoPortal.current} onClose={onCloseDetails} />
|
<RegionInfo
|
||||||
|
region={details.region}
|
||||||
|
mapInfoPortal={mapInfoPortal.current}
|
||||||
|
onClose={onCloseDetails}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Map>
|
</Map>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state) => ({login: state.login}))(MapPage)
|
export default connect((state) => ({ login: state.login }))(MapPage);
|
||||||
|
|
|
@ -32,6 +32,13 @@
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-block-start: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.mapToolbar {
|
.mapToolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { default as AcknowledgementsPage } from "./AcknowledgementsPage";
|
||||||
export { default as ExportPage } from "./ExportPage";
|
export { default as ExportPage } from "./ExportPage";
|
||||||
export { default as HomePage } from "./HomePage";
|
export { default as HomePage } from "./HomePage";
|
||||||
export { default as LoginRedirectPage } from "./LoginRedirectPage";
|
export { default as LoginRedirectPage } from "./LoginRedirectPage";
|
||||||
|
|
|
@ -134,6 +134,11 @@ NotFoundPage:
|
||||||
|
|
||||||
MapPage:
|
MapPage:
|
||||||
sidebar:
|
sidebar:
|
||||||
|
copyright:
|
||||||
|
learnMore: Mehr erfahren
|
||||||
|
openStreetMap: © OpenStreetMap
|
||||||
|
boundaries: © EuroGeographics bezüglich der Verwaltungsgrenzen
|
||||||
|
|
||||||
baseMap:
|
baseMap:
|
||||||
style:
|
style:
|
||||||
label: Stil der Basiskarte
|
label: Stil der Basiskarte
|
||||||
|
@ -365,3 +370,42 @@ RegionStats:
|
||||||
title: Top-Regionen
|
title: Top-Regionen
|
||||||
regionName: Region
|
regionName: Region
|
||||||
eventCount: Anzahl Überholungen
|
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:
|
MapPage:
|
||||||
sidebar:
|
sidebar:
|
||||||
|
copyright:
|
||||||
|
learnMore: Learn more
|
||||||
|
openStreetMap: © OpenStreetMap
|
||||||
|
boundaries: © EuroGeographics for the administrative boundaries
|
||||||
|
|
||||||
baseMap:
|
baseMap:
|
||||||
style:
|
style:
|
||||||
label: Basemap Style
|
label: Basemap Style
|
||||||
|
@ -363,3 +368,38 @@ RegionStats:
|
||||||
title: Top regions
|
title: Top regions
|
||||||
regionName: Region name
|
regionName: Region name
|
||||||
eventCount: Event count
|
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:
|
MapPage:
|
||||||
sidebar:
|
sidebar:
|
||||||
|
copyright:
|
||||||
|
learnMore: En savoir plus
|
||||||
|
openStreetMap: © OpenStreetMap
|
||||||
|
boundaries: © EuroGeographics pour les limites administratives
|
||||||
|
|
||||||
baseMap:
|
baseMap:
|
||||||
style:
|
style:
|
||||||
label: Style de fond de carte
|
label: Style de fond de carte
|
||||||
|
@ -361,3 +366,40 @@ RegionStats:
|
||||||
title: Top régions
|
title: Top régions
|
||||||
regionName: Nom de la région
|
regionName: Nom de la région
|
||||||
eventCount: Nombre de dépassements
|
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
|
FULL OUTER JOIN road ON road.way_id = overtaking_event.way_id
|
||||||
JOIN track on track.id = overtaking_event.track_id
|
JOIN track on track.id = overtaking_event.track_id
|
||||||
WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox
|
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 (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);
|
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)
|
CREATE OR REPLACE FUNCTION layer_obs_regions(bbox geometry, zoom_level int)
|
||||||
RETURNS TABLE(
|
RETURNS TABLE(
|
||||||
region_id bigint,
|
region_id int,
|
||||||
geometry geometry,
|
geometry geometry,
|
||||||
name text,
|
name text,
|
||||||
admin_level int,
|
|
||||||
overtaking_event_count int
|
overtaking_event_count int
|
||||||
) AS $$
|
) AS $$
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
region.relation_id::bigint as region_id,
|
-- region.id as region_id,
|
||||||
ST_SimplifyPreserveTopology(region.geometry, ZRes(zoom_level + 2)) as geometry,
|
NULL::int as region_id,
|
||||||
|
-- ST_SimplifyPreserveTopology(region.geometry, ZRes(zoom_level + 2)) as geometry,
|
||||||
|
region.geometry as geometry,
|
||||||
region.name as name,
|
region.name as name,
|
||||||
region.admin_level as admin_level,
|
|
||||||
count(overtaking_event.id)::int as overtaking_event_count
|
count(overtaking_event.id)::int as overtaking_event_count
|
||||||
FROM region
|
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
|
WHERE
|
||||||
zoom_level >= 4 AND
|
zoom_level >= 3 AND
|
||||||
zoom_level <= 12 AND
|
zoom_level <= 12 AND
|
||||||
region.admin_level = 6 AND
|
|
||||||
region.geometry && bbox
|
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;
|
$$ LANGUAGE SQL IMMUTABLE;
|
||||||
|
|
|
@ -8,8 +8,6 @@ layer:
|
||||||
Number of overtaking events.
|
Number of overtaking events.
|
||||||
name: |
|
name: |
|
||||||
Name of the region
|
Name of the region
|
||||||
admin_level: |
|
|
||||||
Administrative level of the boundary, as tagged in OpenStreetMap
|
|
||||||
defaults:
|
defaults:
|
||||||
srs: EPSG:3785
|
srs: EPSG:3785
|
||||||
datasource:
|
datasource:
|
||||||
|
@ -17,7 +15,7 @@ layer:
|
||||||
geometry_field: geometry
|
geometry_field: geometry
|
||||||
key_field: region_id
|
key_field: region_id
|
||||||
key_field_as_attribute: no
|
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:
|
schema:
|
||||||
- ./layer.sql
|
- ./layer.sql
|
||||||
|
|
Loading…
Reference in a new issue