diff --git a/api/migrations/versions/a049e5eb24dd_create_table_region.py b/api/migrations/versions/a049e5eb24dd_create_table_region.py index 7e50667..1970967 100644 --- a/api/migrations/versions/a049e5eb24dd_create_table_region.py +++ b/api/migrations/versions/a049e5eb24dd_create_table_region.py @@ -13,7 +13,7 @@ from migrations.utils import dbtype # revision identifiers, used by Alembic. revision = "a049e5eb24dd" -down_revision = "a9627f63fbed" +down_revision = "99a3d2eb08f9" branch_labels = None depends_on = None diff --git a/api/obs/api/db.py b/api/obs/api/db.py index 585bf1c..f7716a6 100644 --- a/api/obs/api/db.py +++ b/api/obs/api/db.py @@ -408,29 +408,6 @@ class User(Base): self.username = new_name - async def rename(self, config, new_name): - old_name = self.username - - renames = [ - (join(basedir, old_name), join(basedir, new_name)) - for basedir in [config.PROCESSING_OUTPUT_DIR, config.TRACKS_DIR] - ] - - for src, dst in renames: - if exists(dst): - raise FileExistsError( - f"cannot move {src!r} to {dst!r}, destination exists" - ) - - for src, dst in renames: - if not exists(src): - log.debug("Rename user %s: Not moving %s, not found", self.id, src) - else: - log.info("Rename user %s: Moving %s to %s", self.id, src, dst) - os.rename(src, dst) - - self.username = new_name - class Comment(Base): __tablename__ = "comment" diff --git a/api/obs/api/routes/tiles.py b/api/obs/api/routes/tiles.py index 3579687..9b6b652 100644 --- a/api/obs/api/routes/tiles.py +++ b/api/obs/api/routes/tiles.py @@ -1,6 +1,10 @@ from gzip import decompress from sqlite3 import connect -from sanic.exceptions import Forbidden +from datetime import datetime, time, timedelta +from typing import Optional, Tuple + +import dateutil.parser +from sanic.exceptions import Forbidden, InvalidUsage from sanic.response import raw from sqlalchemy import select, text @@ -89,10 +93,6 @@ async def tiles(req, zoom: int, x: int, y: str): else: user_id, start, end = get_filter_options(req) - parse_date = lambda s: dateutil.parser.parse(s) - start = req.ctx.get_single_arg("start", default=None, convert=parse_date) - end = req.ctx.get_single_arg("end", default=None, convert=parse_date) - tile = await req.ctx.db.scalar( text( f"select data from getmvt(:zoom, :x, :y, :user_id, :min_time, :max_time) as b(data, key);" diff --git a/frontend/src/components/RegionStats/index.tsx b/frontend/src/components/RegionStats/index.tsx index 5dac6b8..1282531 100644 --- a/frontend/src/components/RegionStats/index.tsx +++ b/frontend/src/components/RegionStats/index.tsx @@ -14,6 +14,7 @@ import { useObservable } from "rxjs-hooks"; import { of, from, concat, combineLatest } from "rxjs"; import { map, switchMap, distinctUntilChanged } from "rxjs/operators"; import { Duration, DateTime } from "luxon"; +import {useTranslation} from 'react-i18next' import api from "api"; @@ -26,6 +27,8 @@ function formatDuration(seconds) { } export default function Stats() { + const {t} = useTranslation() + const [page, setPage] = useState(1); const PER_PAGE = 10; const stats = useObservable( @@ -40,7 +43,7 @@ export default function Stats() { return ( <> -
Top Regions
+
{t(`Stats.topRegions`)}
@@ -48,8 +51,8 @@ export default function Stats() { - Region name - Event count + {t(`Stats.regionName`)} + {t(`Stats.eventCount`)} diff --git a/frontend/src/mapstyles/index.js b/frontend/src/mapstyles/index.js index 4e75ad4..abbf89e 100644 --- a/frontend/src/mapstyles/index.js +++ b/frontend/src/mapstyles/index.js @@ -130,7 +130,7 @@ export const getRegionLayers = (adminLevel = 6, baseColor = "#00897B", maxValue "source": "obs", "source-layer": "obs_regions", "minzoom": 0, - "maxzoom": 10, + "maxzoom": 11, "filter": [ "all", ["==", "admin_level", adminLevel], @@ -162,7 +162,7 @@ export const getRegionLayers = (adminLevel = 6, baseColor = "#00897B", maxValue "source": "obs", "source-layer": "obs_regions", "minzoom": 0, - "maxzoom": 10, + "maxzoom": 11, "filter": [ "all", ["==", "admin_level", adminLevel], diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index cc1babb..2085330 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,15 +1,15 @@ import React from 'react' -import {Link} from 'react-router-dom' -import {Message, Grid, Loader, Header, Item} from 'semantic-ui-react' +import {Grid, Loader, Header, Item} from 'semantic-ui-react' import {useObservable} from 'rxjs-hooks' import {of, from} from 'rxjs' import {map, switchMap} from 'rxjs/operators' import {useTranslation} from 'react-i18next' import api from 'api' -import {Stats, Page} from 'components' +import {RegionStats, Stats, Page} from 'components' +import type {Track} from 'types' -import {TrackListItem} from './TracksPage' +import {TrackListItem, NoPublicTracksMessage} from './TracksPage' function MostRecentTrack() { const {t} = useTranslation() @@ -26,19 +26,13 @@ function MostRecentTrack() { return ( <> -
Most recent tracks
- - {tracks?.length === 0 ? ( - - - No public tracks yet. Upload the first! - - - ) : tracks ? ( +
{t('HomePage.mostRecentTrack')}
+ + {track === undefined ? ( + + ) : track ? ( - {tracks.map((track) => ( - - ))} + ) : null} @@ -52,6 +46,7 @@ export default function HomePage() { + diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index 42b7f1e..e92416a 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -24,14 +24,6 @@ import { ColorMapLegend, DiscreteColorMapLegend } from "components"; const BASEMAP_STYLE_OPTIONS = ["positron", "bright"]; const ROAD_ATTRIBUTE_OPTIONS = [ - {value: 'distance_overtaker_mean', key: 'distance_overtaker_mean', text: 'Overtaker distance mean'}, - {value: 'distance_overtaker_min', key: 'distance_overtaker_min', text: 'Overtaker distance minimum'}, - {value: 'distance_overtaker_max', key: 'distance_overtaker_max', text: 'Overtaker distance maximum'}, - {value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'}, - {value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'}, - {value: 'usage_count', key: 'usage_count', text: 'Usage count'}, - {value: 'zone', key: 'zone', text: 'Overtaking distance zone'} -] "distance_overtaker_mean", "distance_overtaker_min", "distance_overtaker_max", @@ -41,11 +33,7 @@ const ROAD_ATTRIBUTE_OPTIONS = [ "zone", ]; -const DATE_FILTER_MODES = [ - { value: "none", key: "none", text: "All time" }, - { value: "range", key: "range", text: "Start and end range" }, - { value: "threshold", key: "threshold", text: "Before/after comparison" }, -]; +const DATE_FILTER_MODES = ["none", "range", "threshold"]; type User = Object; @@ -183,21 +171,6 @@ function LayerSidebar({ /> - ) : ( - - - - ) : attribute.endsWith("zone") ? ( <> diff --git a/frontend/src/pages/MapPage/RegionInfo.tsx b/frontend/src/pages/MapPage/RegionInfo.tsx index cf29948..89ba5fa 100644 --- a/frontend/src/pages/MapPage/RegionInfo.tsx +++ b/frontend/src/pages/MapPage/RegionInfo.tsx @@ -2,14 +2,16 @@ import React, { useState, useCallback } from "react"; import { createPortal } from "react-dom"; import _ from "lodash"; import { List, Header, Icon, Button } from "semantic-ui-react"; +import { useTranslation } from "react-i18next"; import styles from "./styles.module.less"; export default function RegionInfo({ region, mapInfoPortal, onClose }) { + const { t } = useTranslation(); const content = ( <>
-
{region.properties.name || "Unnamed region"}
+
{region.properties.name || t(`MapPage.regionInfo.unnamedRegion`)}
@@ -17,7 +19,7 @@ export default function RegionInfo({ region, mapInfoPortal, onClose }) { - Number of events + {t(`MapPage.regionInfo.eventNumber`)} {region.properties.overtaking_event_count ?? 0} diff --git a/frontend/src/pages/MapPage/RoadInfo.tsx b/frontend/src/pages/MapPage/RoadInfo.tsx index 186ebe7..a4c988a 100644 --- a/frontend/src/pages/MapPage/RoadInfo.tsx +++ b/frontend/src/pages/MapPage/RoadInfo.tsx @@ -1,15 +1,26 @@ -import React, {useState, useCallback} from 'react' -import _ from 'lodash' -import {Segment, Menu, Header, Label, Icon, Table} from 'semantic-ui-react' -import {Layer, Source} from 'react-map-gl' -import {of, from, concat} from 'rxjs' -import {useObservable} from 'rxjs-hooks' -import {switchMap, distinctUntilChanged} from 'rxjs/operators' -import {Chart} from 'components' -import {pairwise} from 'utils' +import React, { useState, useCallback } from "react"; +import _ from "lodash"; +import { + Segment, + Menu, + Header, + Label, + Icon, + Table, + Message, + Button, +} from "semantic-ui-react"; +import { Layer, Source } from "react-map-gl"; +import { of, from, concat } from "rxjs"; +import { useObservable } from "rxjs-hooks"; +import { switchMap, distinctUntilChanged } from "rxjs/operators"; +import { Chart } from "components"; +import { pairwise } from "utils"; +import { useTranslation } from "react-i18next"; -import api from 'api' -import {colorByDistance, borderByZone} from 'mapstyles' +import type { Location } from "types"; +import api from "api"; +import { colorByDistance, borderByZone } from "mapstyles"; import styles from "./styles.module.less"; @@ -127,7 +138,15 @@ function HistogramChart({ bins, counts, zone }) { ); } -export default function RoadInfo({ clickLocation }) { +export default function RoadInfo({ + clickLocation, + hasFilters, + onClose, +}: { + clickLocation: Location | null; + hasFilters: boolean; + onClose: () => void; +}) { const { t } = useTranslation(); const [direction, setDirection] = useState("forwards"); @@ -196,42 +215,51 @@ export default function RoadInfo({ clickLocation }) { /> + {hasFilters && ( + + + + {t("MapPage.roadInfo.hintFiltersNotApplied")} + + + )} + {info?.road.zone && ( )} - {info.road.oneway && ( + {info?.road.oneway && ( )} - {info.road.oneway ? null : ( - + {info?.road.oneway ? null : ( + {t("MapPage.roadInfo.direction")} - {getCardinalDirection(t, info.forwards?.bearing)} + {getCardinalDirection(t, info?.forwards?.bearing)} - {getCardinalDirection(t, info.backwards?.bearing)} + {getCardinalDirection(t, info?.backwards?.bearing)} )} - {info[direction] && } + {info?.[direction] && } - {info[direction]?.distanceOvertaker?.histogram && ( + {info?.[direction]?.distanceOvertaker?.histogram && ( <>
{t("MapPage.roadInfo.overtakerDistanceDistribution")} @@ -246,7 +274,7 @@ export default function RoadInfo({ clickLocation }) { return ( <> - {info.road && ( + {info?.road && ( )} - {content && mapInfoPortal && ( - createPortal( + {content && (
- {content} -
, mapInfoPortal))} + {content} +
)} ); diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx index cdad0ea..a57fb44 100644 --- a/frontend/src/pages/MapPage/index.tsx +++ b/frontend/src/pages/MapPage/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useCallback, useMemo, useRef } from "react"; import _ from "lodash"; import { connect } from "react-redux"; import { Button } from "semantic-ui-react"; @@ -6,6 +6,7 @@ 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"; @@ -15,6 +16,7 @@ import { borderByZone, reds, isValidAttribute, + getRegionLayers } from "mapstyles"; import { useMapConfig } from "reducers/mapConfig"; @@ -69,8 +71,8 @@ const getRoadsLayer = (colorAttribute, maxCount) => : colorAttribute.endsWith("zone") ? borderByZone() : "#DDD"; - draft.paint["line-opacity"][3] = 12; - draft.paint["line-opacity"][5] = 13; + draft.paint["line-opacity"][3] = 10; + draft.paint["line-opacity"][5] = 11; }); const getEventsLayer = () => ({ @@ -193,10 +195,9 @@ function MapPage({ login }) { node = node.parentNode; } - setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]}) const { zoom } = viewportRef.current; - if (zoom < 10) { + if (zoom < 11) { const clickedRegion = e.features?.find( (f) => f.source === "obs" && f.sourceLayer === "obs_regions" ); @@ -204,12 +205,9 @@ function MapPage({ login }) { clickedRegion ? { type: "region", region: clickedRegion } : null ); } else { - const road = await api.get("/mapdetails/road", { - query: { - longitude: e.lngLat[0], - latitude: e.lngLat[1], - radius: 100, - }, + setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]}) + } + }, [setClickLocation] ); const onCloseRoadInfo = useCallback(() => { @@ -292,33 +290,6 @@ function MapPage({ login }) { } ); - const tiles = obsMapSource?.tiles?.map((tileUrl: string) => { - const query = new URLSearchParams(); - if (login) { - if (mapConfig.filters.currentUser) { - query.append("user", login.id); - } - - if (mapConfig.filters.dateMode === "range") { - if (mapConfig.filters.startDate) { - query.append("start", mapConfig.filters.startDate); - } - if (mapConfig.filters.endDate) { - query.append("end", mapConfig.filters.endDate); - } - } else if (mapConfig.filters.dateMode === "threshold") { - if (mapConfig.filters.startDate) { - query.append( - mapConfig.filters.thresholdAfter ? "start" : "end", - mapConfig.filters.startDate - ); - } - } - } - const queryString = String(query); - return tileUrl + (queryString ? "?" : "") + queryString; - }); - const hasFilters: boolean = login && (mapConfig.filters.currentUser || mapConfig.filters.dateMode !== "none"); @@ -330,6 +301,7 @@ function MapPage({ login }) { styles.mapContainer, banner ? styles.hasBanner : null )} + ref={mapInfoPortal} > {layerSidebar && (
@@ -338,6 +310,7 @@ function MapPage({ login }) { )}
+
diff --git a/frontend/src/pages/TracksPage.tsx b/frontend/src/pages/TracksPage.tsx index 3942329..68e3a80 100644 --- a/frontend/src/pages/TracksPage.tsx +++ b/frontend/src/pages/TracksPage.tsx @@ -1,11 +1,20 @@ -import React, {useCallback} from 'react' -import {connect} from 'react-redux' -import {Button, Message, Item, Header, Loader, Pagination, Icon} from 'semantic-ui-react' -import {useObservable} from 'rxjs-hooks' -import {Link} from 'react-router-dom' -import {of, from, concat} from 'rxjs' -import {map, switchMap, distinctUntilChanged} from 'rxjs/operators' -import _ from 'lodash' +import React, { useCallback } from "react"; +import { connect } from "react-redux"; +import { + Button, + Message, + Item, + Header, + Loader, + Pagination, + Icon, +} from "semantic-ui-react"; +import { useObservable } from "rxjs-hooks"; +import { Link } from "react-router-dom"; +import { of, from, concat } from "rxjs"; +import { map, switchMap, distinctUntilChanged } from "rxjs/operators"; +import _ from "lodash"; +import { useTranslation, Trans as Translate } from "react-i18next"; import type { Track } from "types"; import { @@ -82,7 +91,7 @@ export function NoPublicTracksMessage() { ); } -function maxLength(t: string|null, max: number): string|null { +function maxLength(t: string | null, max: number): string | null { if (t && t.length > max) { return t.substring(0, max) + " ..."; } else { diff --git a/frontend/src/translations/de.yaml b/frontend/src/translations/de.yaml index db25376..c9df3a5 100644 --- a/frontend/src/translations/de.yaml +++ b/frontend/src/translations/de.yaml @@ -62,6 +62,9 @@ Stats: thisMonth: Dieser Monat thisYear: Dieses Jahr allTime: Immer + topRegions: Regionen mit den meisten Messwerten + eventCount: Überholevents + regionName: Region TracksPage: titlePublic: Öffentliche Fahrten @@ -201,6 +204,9 @@ MapPage: southWest: südwestwärts west: westwärts northWest: nordwestwärts + regionInfo: + unnamedRegion: Unbenannte Region + eventNumber: Anzahl Überholevents SettingsPage: title: Einstellungen diff --git a/frontend/src/translations/en.yaml b/frontend/src/translations/en.yaml index 1246052..831d999 100644 --- a/frontend/src/translations/en.yaml +++ b/frontend/src/translations/en.yaml @@ -67,6 +67,9 @@ Stats: thisMonth: This month thisYear: This year allTime: All time + topRegions: Region Leaderboard + eventCount: Overtaking events + regionName: Region TracksPage: titlePublic: Public tracks @@ -206,6 +209,10 @@ MapPage: southWest: south-west bound west: west bound northWest: north-west bound + regionInfo: + unnamedRegion: "unnamed region" + eventNumber: Number of Overtaking events + SettingsPage: title: Settings diff --git a/tile-generator/layers/obs_events/layer.sql b/tile-generator/layers/obs_events/layer.sql index ec6c9af..4ba0f12 100644 --- a/tile-generator/layers/obs_events/layer.sql +++ b/tile-generator/layers/obs_events/layer.sql @@ -12,11 +12,10 @@ RETURNS TABLE(event_id bigint, geometry geometry, distance_overtaker float, dist CASE WHEN road.zone IS NULL THEN 'urban' else road.zone END as zone, overtaking_event.way_id::bigint as way_id FROM overtaking_event - 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 - WHERE - zoom_level >= 10 AND - ST_Transform(overtaking_event.geometry, 3857) && bbox; + WHERE ST_Transform(overtaking_event.geometry, 3857) && bbox + AND zoom_level >= 12 AND (user_id is NULL OR user_id = track.author_id) AND time BETWEEN COALESCE(min_time, '1900-01-01'::timestamp) AND COALESCE(max_time, '2100-01-01'::timestamp); diff --git a/tile-generator/layers/obs_roads/layer.sql b/tile-generator/layers/obs_roads/layer.sql index 391bfee..cd12a95 100644 --- a/tile-generator/layers/obs_roads/layer.sql +++ b/tile-generator/layers/obs_roads/layer.sql @@ -67,7 +67,7 @@ RETURNS TABLE( ) e on (e.way_id = road.way_id and (road.directionality != 0 or e.direction_reversed = r.rev)) WHERE - zoom_level >= 10 AND + zoom_level >= 11 AND road.geometry && bbox GROUP BY road.name,